chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.1.2
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = train-v{new_version}
|
||||
message = Bump cua-train to v{new_version}
|
||||
|
||||
[bumpversion:file:pyproject.toml]
|
||||
search = version = "{current_version}"
|
||||
replace = version = "{new_version}"
|
||||
@@ -0,0 +1,23 @@
|
||||
# cua-train
|
||||
|
||||
Python client for the [Cua Cloud](https://run.cua.ai) API.
|
||||
|
||||
```bash
|
||||
pip install cua-train
|
||||
```
|
||||
|
||||
```python
|
||||
from cua_train import TrainClient
|
||||
|
||||
client = TrainClient.from_key(
|
||||
client_id="ukey-...", # from your Cua Cloud account
|
||||
client_secret="...",
|
||||
)
|
||||
|
||||
# Control plane — claim a VM
|
||||
http = client.get_async_httpx_client()
|
||||
await http.post("/api/k8s/apis/osgym.cua.ai/v1alpha1/namespaces/my-pool/osgymsandboxclaims", json={...})
|
||||
|
||||
# Data plane — exec in the VM
|
||||
await http.post("/api/svc/my-pool/my-sandbox-server/execute", json={"command": "echo hi"})
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
[project]
|
||||
name = "cua-train"
|
||||
version = "0.1.2"
|
||||
description = "Cua Cloud Python client — control and data plane for Cua Cloud desktop VM pools"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "TryCua", email = "hello@trycua.com" }
|
||||
]
|
||||
keywords = [
|
||||
"cua",
|
||||
"computer-use",
|
||||
"cloud",
|
||||
"vm",
|
||||
"automation",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["httpx>=0.27.0"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://run.cua.ai"
|
||||
Repository = "https://github.com/trycua/cua"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/cua_train"]
|
||||
@@ -0,0 +1,5 @@
|
||||
"""cua-train — Python client for the Cua Cloud API."""
|
||||
|
||||
from .client import TrainClient
|
||||
|
||||
__all__ = ["TrainClient"]
|
||||
@@ -0,0 +1,117 @@
|
||||
"""TrainClient — authenticated httpx client for the Cua Cloud API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class TrainClient:
|
||||
"""Authenticated client for the Cua Cloud API.
|
||||
|
||||
Obtain via :meth:`from_key` using a per-user key (``ukey-…``) from
|
||||
your Cua Cloud account. The client transparently refreshes its bearer
|
||||
token before it expires.
|
||||
|
||||
Both the control plane (VM claim lifecycle via ``/api/k8s``) and the
|
||||
data plane (exec/upload/screenshot via ``/api/svc``) are reached through
|
||||
the same base URL (default: ``https://run.cua.ai``).
|
||||
"""
|
||||
|
||||
_DEFAULT_TOKEN_URL = "https://auth.cua.ai/realms/cyclops-cs/protocol/openid-connect/token"
|
||||
_DEFAULT_BASE_URL = "https://run.cua.ai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
base_url: str = _DEFAULT_BASE_URL,
|
||||
*,
|
||||
token_url: str = _DEFAULT_TOKEN_URL,
|
||||
client_id: str = "",
|
||||
client_secret: str = "",
|
||||
token_deadline: float = 0.0,
|
||||
) -> None:
|
||||
self._token = token
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._token_url = token_url
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._token_deadline = token_deadline
|
||||
self._refresh_lock = threading.Lock()
|
||||
self._async_client: httpx.AsyncClient | None = None
|
||||
|
||||
@classmethod
|
||||
def from_key(
|
||||
cls,
|
||||
*,
|
||||
token_url: str = _DEFAULT_TOKEN_URL,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
base_url: str = _DEFAULT_BASE_URL,
|
||||
) -> "TrainClient":
|
||||
"""Exchange client credentials for a bearer token.
|
||||
|
||||
Args:
|
||||
token_url: Keycloak token endpoint.
|
||||
client_id: Per-user key client ID (``ukey-…``).
|
||||
client_secret: Client secret.
|
||||
base_url: Cua Cloud API base URL (default: ``https://run.cua.ai``).
|
||||
"""
|
||||
token, ttl = cls._exchange(token_url, client_id, client_secret)
|
||||
return cls(
|
||||
token=token,
|
||||
base_url=base_url,
|
||||
token_url=token_url,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
token_deadline=time.monotonic() + ttl,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _exchange(token_url: str, client_id: str, client_secret: str) -> tuple[str, float]:
|
||||
resp = httpx.post(
|
||||
token_url,
|
||||
data={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
body = resp.json()
|
||||
return body["access_token"], float(body.get("expires_in", 300))
|
||||
|
||||
def reset_token(self) -> None:
|
||||
"""Force re-exchange on the next :meth:`get_async_httpx_client` call."""
|
||||
self._token_deadline = 0
|
||||
|
||||
def _refresh_token(self) -> None:
|
||||
if not self._client_id:
|
||||
return
|
||||
if time.monotonic() < self._token_deadline - 30:
|
||||
return
|
||||
with self._refresh_lock:
|
||||
if time.monotonic() < self._token_deadline - 30:
|
||||
return
|
||||
token, ttl = self._exchange(self._token_url, self._client_id, self._client_secret)
|
||||
self._token = token
|
||||
self._token_deadline = time.monotonic() + ttl
|
||||
auth_header = f"Bearer {token}"
|
||||
if self._async_client is not None:
|
||||
self._async_client.headers["Authorization"] = auth_header
|
||||
|
||||
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
||||
"""Return an async httpx client with the current bearer token.
|
||||
|
||||
Re-mints the token transparently when it is about to expire.
|
||||
"""
|
||||
self._refresh_token()
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(
|
||||
base_url=self._base_url,
|
||||
headers={"Authorization": f"Bearer {self._token}"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
return self._async_client
|
||||
Reference in New Issue
Block a user