Files
trycua--cua/libs/python/cua-cli/cua_cli/api/client.py
T
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

145 lines
4.7 KiB
Python

"""HTTP API client for CUA cloud services."""
import hashlib
import os
from pathlib import Path
from typing import Any, Optional
from urllib.parse import quote
import aiohttp
from cua_cli.auth.store import require_api_key
from cua_core.http import cua_version_headers
DEFAULT_API_BASE = "https://api.cua.ai"
def get_api_base() -> str:
"""Get the API base URL."""
return os.environ.get("CUA_API_BASE", DEFAULT_API_BASE).rstrip("/")
class CloudAPIClient:
"""HTTP client for CUA cloud API."""
def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None):
self.api_key = api_key or require_api_key()
self.api_base = api_base or get_api_base()
def _headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
**cua_version_headers(),
}
async def _request(
self,
method: str,
path: str,
json: Optional[dict] = None,
timeout: int = 30,
) -> tuple[int, Any]:
"""Make an HTTP request to the API.
Returns:
Tuple of (status_code, response_data)
"""
url = f"{self.api_base}{path}"
headers = self._headers()
if json is not None:
headers["Content-Type"] = "application/json"
async with aiohttp.ClientSession() as session:
timeout_obj = aiohttp.ClientTimeout(total=timeout)
async with session.request(
method, url, headers=headers, json=json, timeout=timeout_obj
) as resp:
try:
data = await resp.json(content_type=None)
except Exception:
data = await resp.text()
return resp.status, data
# Image API methods
async def list_images(self) -> list[dict[str, Any]]:
"""List all images in the workspace."""
status, data = await self._request("GET", "/v1/images")
if status == 200 and isinstance(data, list):
return data
return []
async def initiate_upload(
self,
name: str,
tag: str,
image_type: str,
size_bytes: int,
checksum_sha256: str,
) -> tuple[int, dict[str, Any]]:
"""Initiate a multi-part upload session.
Returns:
Tuple of (status_code, session_data)
"""
path = f"/v1/images/{quote(name, safe='')}/upload"
body = {
"tag": tag,
"image_type": image_type,
"size_bytes": size_bytes,
"checksum_sha256": checksum_sha256,
}
return await self._request("POST", path, json=body)
async def get_upload_part_url(
self, name: str, upload_id: str, part_number: int
) -> tuple[int, dict[str, Any]]:
"""Get a signed URL for uploading a part."""
path = f"/v1/images/{quote(name, safe='')}/upload/{upload_id}/part/{part_number}"
return await self._request("GET", path)
async def complete_upload(
self, name: str, upload_id: str, parts: list[dict[str, Any]]
) -> tuple[int, dict[str, Any]]:
"""Complete a multi-part upload."""
path = f"/v1/images/{quote(name, safe='')}/upload/{upload_id}/complete"
return await self._request("POST", path, json={"parts": parts})
async def abort_upload(self, name: str, upload_id: str) -> tuple[int, Any]:
"""Abort an upload session."""
path = f"/v1/images/{quote(name, safe='')}/upload/{upload_id}"
return await self._request("DELETE", path)
async def get_download_url(self, name: str, tag: str) -> tuple[int, dict[str, Any]]:
"""Get a signed URL for downloading an image."""
path = f"/v1/images/{quote(name, safe='')}/download?tag={quote(tag, safe='')}"
return await self._request("GET", path)
async def delete_image(self, name: str, tag: str) -> tuple[int, Any]:
"""Delete an image version."""
path = f"/v1/images/{quote(name, safe='')}?tag={quote(tag, safe='')}"
return await self._request("DELETE", path)
def calculate_file_hash(file_path: Path) -> str:
"""Calculate SHA256 hash of a file."""
sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest()
def format_bytes(size_bytes: int) -> str:
"""Format bytes in human-readable format."""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB"]
k = 1024
i = 0
while size_bytes >= k and i < len(units) - 1:
size_bytes /= k
i += 1
return f"{size_bytes:.2f} {units[i]}"