211 lines
7.2 KiB
Python
211 lines
7.2 KiB
Python
"""Provider-agnostic API management using system keyring."""
|
|
|
|
import keyring
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.prompt import Prompt
|
|
from rich.panel import Panel
|
|
from openai import OpenAI
|
|
import sys
|
|
|
|
console = Console()
|
|
|
|
# Service name for keyring
|
|
SERVICE_NAME = "taletorrent"
|
|
API_KEY_ITEM = "api_key"
|
|
BASE_URL_ITEM = "base_url"
|
|
|
|
# Default configuration
|
|
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
|
|
|
|
|
class KeyringManager:
|
|
"""Manages secure storage of API keys and base URLs using system keyring."""
|
|
|
|
@staticmethod
|
|
def store_api_config(api_key: str, base_url: str) -> None:
|
|
"""Store API key and base URL securely in system keyring."""
|
|
try:
|
|
keyring.set_password(SERVICE_NAME, API_KEY_ITEM, api_key)
|
|
keyring.set_password(SERVICE_NAME, BASE_URL_ITEM, base_url)
|
|
console.print(
|
|
"✅ [green]API configuration stored securely in system keyring[/green]"
|
|
)
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Error storing API configuration: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
@staticmethod
|
|
def get_api_key() -> str:
|
|
"""Retrieve API key from system keyring."""
|
|
try:
|
|
api_key = keyring.get_password(SERVICE_NAME, API_KEY_ITEM)
|
|
if api_key is None:
|
|
console.print("❌ [red]No API key found in system keyring[/red]")
|
|
console.print(
|
|
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
|
)
|
|
raise typer.Exit(1)
|
|
return api_key
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Error accessing keyring: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
@staticmethod
|
|
def get_base_url() -> str:
|
|
"""Retrieve base URL from system keyring."""
|
|
try:
|
|
base_url = keyring.get_password(SERVICE_NAME, BASE_URL_ITEM)
|
|
if base_url is None:
|
|
console.print("❌ [red]No base URL found in system keyring[/red]")
|
|
console.print(
|
|
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
|
)
|
|
raise typer.Exit(1)
|
|
return base_url
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Error accessing keyring: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
@staticmethod
|
|
def has_api_config() -> bool:
|
|
"""Check if API configuration exists in keyring."""
|
|
try:
|
|
api_key = keyring.get_password(SERVICE_NAME, API_KEY_ITEM)
|
|
base_url = keyring.get_password(SERVICE_NAME, BASE_URL_ITEM)
|
|
return api_key is not None and base_url is not None
|
|
except Exception:
|
|
return False
|
|
|
|
@staticmethod
|
|
def delete_api_config() -> None:
|
|
"""Delete API configuration from system keyring."""
|
|
try:
|
|
keyring.delete_password(SERVICE_NAME, API_KEY_ITEM)
|
|
keyring.delete_password(SERVICE_NAME, BASE_URL_ITEM)
|
|
console.print(
|
|
"✅ [green]API configuration removed from system keyring[/green]"
|
|
)
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Error deleting API configuration: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
|
|
def validate_api_config(api_key: str, base_url: str) -> bool:
|
|
"""Validate API configuration by testing it against endpoint."""
|
|
try:
|
|
client = OpenAI(
|
|
base_url=base_url,
|
|
api_key=api_key,
|
|
)
|
|
|
|
# Test with a minimal request
|
|
response = client.models.list()
|
|
return True
|
|
except Exception as e:
|
|
console.print(f"❌ Connection failed: {str(e)}")
|
|
return False
|
|
|
|
|
|
def setup_api_config_wizard() -> None:
|
|
"""Interactive wizard for setting up API configuration."""
|
|
console.print(
|
|
Panel.fit(
|
|
"[bold cyan]API Configuration Setup[/bold cyan]\n\n"
|
|
"Configure any OpenAI-compatible API endpoint.\n"
|
|
"Your credentials will be stored securely in your system keyring.",
|
|
title="API Provider Setup",
|
|
)
|
|
)
|
|
|
|
# Get base URL
|
|
base_url = Prompt.ask("Enter API base URL", default=DEFAULT_BASE_URL)
|
|
|
|
# Get API key
|
|
api_key = Prompt.ask("Enter API key", password=True)
|
|
|
|
while True:
|
|
if not api_key.strip():
|
|
console.print("❌ [red]API key cannot be empty[/red]")
|
|
api_key = Prompt.ask("Enter API key", password=True)
|
|
continue
|
|
|
|
console.print("🔄 [blue]Validating API configuration...[/blue]")
|
|
|
|
if validate_api_config(api_key, base_url):
|
|
KeyringManager.store_api_config(api_key, base_url)
|
|
console.print(
|
|
"\n✅ [green]API configuration setup complete! You can now start playing.[/green]"
|
|
)
|
|
break
|
|
else:
|
|
console.print("⚠️ [yellow]Warning: API validation failed[/yellow]")
|
|
console.print(
|
|
"[dim]This may be due to network issues, invalid credentials, or backend compatibility issues.[/dim]"
|
|
)
|
|
|
|
choice = Prompt.ask(
|
|
"What would you like to do? [retry/force/cancel]",
|
|
choices=["retry", "force", "cancel"],
|
|
default="retry",
|
|
)
|
|
|
|
if choice == "retry":
|
|
base_url = Prompt.ask("Enter API base URL", default=base_url)
|
|
api_key = Prompt.ask("Enter API key", password=True)
|
|
continue
|
|
elif choice == "force":
|
|
console.print(
|
|
"[yellow]⚠️ Storing configuration without validation[/yellow]"
|
|
)
|
|
console.print(
|
|
"[dim]Note: You may encounter errors when using taletorrent if the configuration is invalid.[/dim]"
|
|
)
|
|
KeyringManager.store_api_config(api_key, base_url)
|
|
console.print(
|
|
"\n✅ [green]API configuration stored (validation skipped)[/green]"
|
|
)
|
|
break
|
|
else: # cancel
|
|
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
break
|
|
|
|
|
|
def get_openai_client():
|
|
"""Get configured OpenAI client with secure API credentials."""
|
|
api_key = KeyringManager.get_api_key()
|
|
base_url = KeyringManager.get_base_url()
|
|
return OpenAI(
|
|
base_url=base_url,
|
|
api_key=api_key,
|
|
)
|
|
|
|
|
|
def ensure_api_config() -> None:
|
|
"""Ensure API configuration exists, prompt for setup if missing."""
|
|
if not KeyringManager.has_api_config():
|
|
console.print("❌ [red]No API configuration found[/red]")
|
|
console.print(
|
|
"Run [cyan]taletorrent config[/cyan] to set up your API configuration"
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
|
|
def get_api_info() -> dict:
|
|
"""Get current API configuration info for display."""
|
|
try:
|
|
if not KeyringManager.has_api_config():
|
|
return None
|
|
|
|
api_key = KeyringManager.get_api_key()
|
|
base_url = KeyringManager.get_base_url()
|
|
|
|
return {
|
|
"base_url": base_url,
|
|
"api_key": api_key[:8] + "*" * (len(api_key) - 12) + api_key[-4:]
|
|
if len(api_key) > 12
|
|
else "*" * len(api_key),
|
|
}
|
|
except Exception:
|
|
return None
|