Files
taletorrent/taletorrent/api_key.py

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