"""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