Complete Python CLI for OpenWebUI with: - Auth commands (login, logout, whoami, token, refresh) - Chat send with streaming support - RAG files/collections management - Models list/info - Admin stats (minimal v1.0) - Config management with profiles and keyring Stack: typer, httpx, rich, pydantic, keyring Based on RFC v1.2 with 22-step implementation checklist 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
151 lines
4.3 KiB
Python
151 lines
4.3 KiB
Python
"""HTTP client wrapper for OpenWebUI API."""
|
|
|
|
from typing import Any, AsyncIterator
|
|
|
|
import httpx
|
|
import keyring
|
|
|
|
from .config import get_effective_config, load_config
|
|
from .errors import AuthError, NetworkError, ServerError
|
|
|
|
|
|
KEYRING_SERVICE = "openwebui-cli"
|
|
|
|
|
|
def get_token(profile: str, uri: str) -> str | None:
|
|
"""Retrieve token from system keyring."""
|
|
key = f"{profile}:{uri}"
|
|
return keyring.get_password(KEYRING_SERVICE, key)
|
|
|
|
|
|
def set_token(profile: str, uri: str, token: str) -> None:
|
|
"""Store token in system keyring."""
|
|
key = f"{profile}:{uri}"
|
|
keyring.set_password(KEYRING_SERVICE, key, token)
|
|
|
|
|
|
def delete_token(profile: str, uri: str) -> None:
|
|
"""Delete token from system keyring."""
|
|
key = f"{profile}:{uri}"
|
|
try:
|
|
keyring.delete_password(KEYRING_SERVICE, key)
|
|
except keyring.errors.PasswordDeleteError:
|
|
pass # Token doesn't exist, that's fine
|
|
|
|
|
|
def create_client(
|
|
profile: str | None = None,
|
|
uri: str | None = None,
|
|
token: str | None = None,
|
|
timeout: float | None = None,
|
|
) -> httpx.Client:
|
|
"""
|
|
Create an HTTP client configured for OpenWebUI API.
|
|
|
|
Args:
|
|
profile: Profile name to use
|
|
uri: Override server URI
|
|
token: Override token (otherwise uses keyring)
|
|
timeout: Request timeout in seconds
|
|
|
|
Returns:
|
|
Configured httpx.Client
|
|
"""
|
|
effective_uri, effective_profile = get_effective_config(profile, uri)
|
|
config = load_config()
|
|
|
|
# Get token from keyring if not provided
|
|
if token is None:
|
|
token = get_token(effective_profile, effective_uri)
|
|
|
|
# Build headers
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
|
|
# Use config timeout if not specified
|
|
if timeout is None:
|
|
timeout = config.defaults.timeout
|
|
|
|
return httpx.Client(
|
|
base_url=effective_uri,
|
|
headers=headers,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def create_async_client(
|
|
profile: str | None = None,
|
|
uri: str | None = None,
|
|
token: str | None = None,
|
|
timeout: float | None = None,
|
|
) -> httpx.AsyncClient:
|
|
"""Create an async HTTP client configured for OpenWebUI API."""
|
|
effective_uri, effective_profile = get_effective_config(profile, uri)
|
|
config = load_config()
|
|
|
|
if token is None:
|
|
token = get_token(effective_profile, effective_uri)
|
|
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
|
|
if timeout is None:
|
|
timeout = config.defaults.timeout
|
|
|
|
return httpx.AsyncClient(
|
|
base_url=effective_uri,
|
|
headers=headers,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def handle_response(response: httpx.Response) -> dict[str, Any]:
|
|
"""
|
|
Handle API response, raising appropriate errors for failures.
|
|
|
|
Returns:
|
|
Parsed JSON response
|
|
|
|
Raises:
|
|
AuthError: For 401/403 responses
|
|
ServerError: For 5xx responses
|
|
CLIError: For other error responses
|
|
"""
|
|
if response.status_code == 401:
|
|
raise AuthError("Authentication required. Please run 'openwebui auth login'")
|
|
elif response.status_code == 403:
|
|
raise AuthError("Permission denied. Check your access rights.")
|
|
elif response.status_code >= 500:
|
|
raise ServerError(f"Server error: {response.status_code} - {response.text}")
|
|
elif response.status_code >= 400:
|
|
try:
|
|
error_data = response.json()
|
|
message = error_data.get("detail", error_data.get("message", response.text))
|
|
except Exception:
|
|
message = response.text
|
|
raise ServerError(f"API error: {response.status_code} - {message}")
|
|
|
|
try:
|
|
return response.json()
|
|
except Exception:
|
|
return {"text": response.text}
|
|
|
|
|
|
def handle_request_error(error: Exception) -> None:
|
|
"""Convert httpx errors to CLI errors."""
|
|
if isinstance(error, httpx.ConnectError):
|
|
raise NetworkError(f"Could not connect to server: {error}")
|
|
elif isinstance(error, httpx.TimeoutException):
|
|
raise NetworkError(f"Request timed out: {error}")
|
|
elif isinstance(error, httpx.RequestError):
|
|
raise NetworkError(f"Request failed: {error}")
|
|
else:
|
|
raise error
|