91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
145 lines
4.7 KiB
Python
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]}"
|