mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310, S306, S110, TID251, UP042, F401) and run black on 526 files. Co-authored-by: Hanzo Dev <[email protected]>
385 lines
12 KiB
Python
385 lines
12 KiB
Python
"""Platform authentication utilities for Hanzo Platform MCP.
|
|
|
|
This module provides utilities for authenticating with Hanzo Platform,
|
|
including:
|
|
- Getting API keys
|
|
- Getting deployment tokens
|
|
- Setting up CI/CD credentials
|
|
|
|
Usage:
|
|
# Interactive login
|
|
hanzo-platform login
|
|
|
|
# Get deploy token for CI
|
|
hanzo-platform deploy-token --app console
|
|
|
|
# Check authentication status
|
|
hanzo-platform status
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import asyncio
|
|
import webbrowser
|
|
from typing import Any, Dict, Optional
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
|
|
import httpx
|
|
|
|
|
|
@dataclass
|
|
class PlatformConfig:
|
|
"""Configuration for Platform API."""
|
|
|
|
base_url: str = "https://platform.hanzo.ai"
|
|
api_key: Optional[str] = None
|
|
org_id: Optional[str] = None
|
|
|
|
@classmethod
|
|
def load(cls) -> "PlatformConfig":
|
|
"""Load configuration from file and environment."""
|
|
config = cls()
|
|
|
|
# Load from file
|
|
config_file = Path.home() / ".hanzo" / "platform" / "config.json"
|
|
if config_file.exists():
|
|
try:
|
|
with open(config_file, "r") as f:
|
|
data = json.load(f)
|
|
config.base_url = data.get("base_url", config.base_url)
|
|
config.api_key = data.get("api_key")
|
|
config.org_id = data.get("org_id")
|
|
except Exception:
|
|
pass
|
|
|
|
# Environment overrides
|
|
if os.environ.get("PLATFORM_URL"):
|
|
config.base_url = os.environ["PLATFORM_URL"]
|
|
if os.environ.get("PLATFORM_API_KEY"):
|
|
config.api_key = os.environ["PLATFORM_API_KEY"]
|
|
if os.environ.get("PLATFORM_ORG_ID"):
|
|
config.org_id = os.environ["PLATFORM_ORG_ID"]
|
|
|
|
return config
|
|
|
|
def save(self):
|
|
"""Save configuration to file."""
|
|
config_file = Path.home() / ".hanzo" / "platform" / "config.json"
|
|
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
data = {
|
|
"base_url": self.base_url,
|
|
"api_key": self.api_key,
|
|
"org_id": self.org_id,
|
|
}
|
|
|
|
with open(config_file, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
# Set restrictive permissions
|
|
try:
|
|
import stat
|
|
|
|
config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class PlatformClient:
|
|
"""Client for Hanzo Platform API."""
|
|
|
|
def __init__(self, config: Optional[PlatformConfig] = None):
|
|
self.config = config or PlatformConfig.load()
|
|
self._client: Optional[httpx.AsyncClient] = None
|
|
|
|
async def __aenter__(self):
|
|
self._client = httpx.AsyncClient(
|
|
base_url=self.config.base_url,
|
|
headers=self._get_headers(),
|
|
timeout=30.0,
|
|
)
|
|
return self
|
|
|
|
async def __aexit__(self, *args):
|
|
if self._client:
|
|
await self._client.aclose()
|
|
|
|
def _get_headers(self) -> Dict[str, str]:
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
}
|
|
if self.config.api_key:
|
|
headers["x-api-key"] = self.config.api_key
|
|
return headers
|
|
|
|
async def check_auth(self) -> Dict[str, Any]:
|
|
"""Check if authentication is valid."""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized")
|
|
|
|
try:
|
|
response = await self._client.get("/api/v1/user/me")
|
|
if response.status_code == 200:
|
|
return {"authenticated": True, "user": response.json()}
|
|
return {"authenticated": False, "error": "Invalid API key"}
|
|
except Exception as e:
|
|
return {"authenticated": False, "error": str(e)}
|
|
|
|
async def list_projects(self) -> Dict[str, Any]:
|
|
"""List all projects."""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized")
|
|
|
|
response = await self._client.get("/api/v1/projects")
|
|
return response.json()
|
|
|
|
async def list_applications(self, project_id: str) -> Dict[str, Any]:
|
|
"""List applications in a project."""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized")
|
|
|
|
response = await self._client.get(f"/api/v1/projects/{project_id}/applications")
|
|
return response.json()
|
|
|
|
async def get_deploy_token(self, app_id: str) -> Optional[str]:
|
|
"""Get or create a deploy token for an application."""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized")
|
|
|
|
# Get application details including refresh token
|
|
response = await self._client.get(f"/api/v1/applications/{app_id}")
|
|
if response.status_code != 200:
|
|
return None
|
|
|
|
app_data = response.json()
|
|
return app_data.get("application", {}).get("refreshToken")
|
|
|
|
async def create_api_key(self, name: str = "hanzo-mcp") -> Optional[str]:
|
|
"""Create a new API key."""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized")
|
|
|
|
response = await self._client.post(
|
|
"/api/v1/api-keys",
|
|
json={"name": name, "expiresAt": None},
|
|
)
|
|
|
|
if response.status_code == 201:
|
|
data = response.json()
|
|
return data.get("apiKey", {}).get("key")
|
|
return None
|
|
|
|
|
|
async def interactive_login() -> bool:
|
|
"""Interactive login flow."""
|
|
print("=== Hanzo Platform Login ===")
|
|
print()
|
|
|
|
config = PlatformConfig.load()
|
|
|
|
# Check if already logged in
|
|
if config.api_key:
|
|
async with PlatformClient(config) as client:
|
|
result = await client.check_auth()
|
|
if result.get("authenticated"):
|
|
user = result.get("user", {})
|
|
print(f"Already logged in as: {user.get('email', 'Unknown')}")
|
|
confirm = input("Re-authenticate? [y/N]: ").strip().lower()
|
|
if confirm != "y":
|
|
return True
|
|
|
|
print(f"Opening {config.base_url}/settings/api-keys in your browser...")
|
|
print()
|
|
|
|
# Try to open browser
|
|
try:
|
|
webbrowser.open(f"{config.base_url}/settings/api-keys")
|
|
except Exception:
|
|
print(
|
|
f"Could not open browser. Please visit: {config.base_url}/settings/api-keys"
|
|
)
|
|
|
|
print("1. Create a new API key in the Platform dashboard")
|
|
print("2. Copy the API key")
|
|
print()
|
|
|
|
api_key = input("Paste your API key here: ").strip()
|
|
|
|
if not api_key:
|
|
print("Error: No API key provided")
|
|
return False
|
|
|
|
# Validate the API key
|
|
config.api_key = api_key
|
|
|
|
async with PlatformClient(config) as client:
|
|
result = await client.check_auth()
|
|
|
|
if result.get("authenticated"):
|
|
user = result.get("user", {})
|
|
print(f"\n✅ Successfully authenticated as: {user.get('email')}")
|
|
|
|
# Save the configuration
|
|
config.save()
|
|
|
|
# Also set in environment for current session
|
|
os.environ["PLATFORM_API_KEY"] = api_key
|
|
|
|
print(f"\nConfiguration saved to: ~/.hanzo/platform/config.json")
|
|
print(f"For CI/CD, set: PLATFORM_API_KEY={api_key[:8]}...")
|
|
return True
|
|
else:
|
|
print(f"\n❌ Authentication failed: {result.get('error')}")
|
|
return False
|
|
|
|
|
|
async def get_deploy_token_cli(app_name: str) -> Optional[str]:
|
|
"""Get deploy token for an application."""
|
|
config = PlatformConfig.load()
|
|
|
|
if not config.api_key:
|
|
print("Error: Not logged in. Run 'hanzo-platform login' first.")
|
|
return None
|
|
|
|
async with PlatformClient(config) as client:
|
|
# List projects to find the app
|
|
projects = await client.list_projects()
|
|
|
|
for project in projects.get("projects", []):
|
|
apps = await client.list_applications(project["id"])
|
|
|
|
for app in apps.get("applications", []):
|
|
if app.get("name") == app_name or app.get("id") == app_name:
|
|
token = await client.get_deploy_token(app["id"])
|
|
if token:
|
|
return token
|
|
|
|
print(f"Error: Application '{app_name}' not found")
|
|
return None
|
|
|
|
|
|
async def status() -> Dict[str, Any]:
|
|
"""Check authentication status and available resources."""
|
|
config = PlatformConfig.load()
|
|
|
|
result = {
|
|
"configured": bool(config.api_key),
|
|
"base_url": config.base_url,
|
|
"authenticated": False,
|
|
"user": None,
|
|
"projects": [],
|
|
}
|
|
|
|
if not config.api_key:
|
|
return result
|
|
|
|
async with PlatformClient(config) as client:
|
|
auth_result = await client.check_auth()
|
|
result["authenticated"] = auth_result.get("authenticated", False)
|
|
result["user"] = auth_result.get("user")
|
|
|
|
if result["authenticated"]:
|
|
try:
|
|
projects = await client.list_projects()
|
|
result["projects"] = projects.get("projects", [])
|
|
except Exception:
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
def cli():
|
|
"""CLI entry point."""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Hanzo Platform authentication",
|
|
prog="hanzo-platform",
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
# Login command
|
|
subparsers.add_parser("login", help="Interactive login")
|
|
|
|
# Status command
|
|
subparsers.add_parser("status", help="Check authentication status")
|
|
|
|
# Deploy token command
|
|
deploy_parser = subparsers.add_parser(
|
|
"deploy-token", help="Get deploy token for app"
|
|
)
|
|
deploy_parser.add_argument("--app", required=True, help="Application name or ID")
|
|
deploy_parser.add_argument("--output", choices=["plain", "json"], default="plain")
|
|
|
|
# Config command
|
|
config_parser = subparsers.add_parser("config", help="Show/set configuration")
|
|
config_parser.add_argument("--set-url", help="Set Platform URL")
|
|
config_parser.add_argument("--set-key", help="Set API key")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "login":
|
|
success = asyncio.run(interactive_login())
|
|
sys.exit(0 if success else 1)
|
|
|
|
elif args.command == "status":
|
|
result = asyncio.run(status())
|
|
|
|
if result["authenticated"]:
|
|
user = result.get("user", {})
|
|
print(f"✅ Authenticated as: {user.get('email', 'Unknown')}")
|
|
print(f" Platform: {result['base_url']}")
|
|
print(f" Projects: {len(result.get('projects', []))}")
|
|
|
|
for project in result.get("projects", []):
|
|
print(f" - {project.get('name', 'Unknown')}")
|
|
else:
|
|
print("❌ Not authenticated")
|
|
print(f" Platform: {result['base_url']}")
|
|
print("\nRun 'hanzo-platform login' to authenticate")
|
|
sys.exit(0)
|
|
|
|
elif args.command == "deploy-token":
|
|
token = asyncio.run(get_deploy_token_cli(args.app))
|
|
if token:
|
|
if args.output == "json":
|
|
print(json.dumps({"token": token}))
|
|
else:
|
|
print(token)
|
|
sys.exit(0)
|
|
sys.exit(1)
|
|
|
|
elif args.command == "config":
|
|
config = PlatformConfig.load()
|
|
|
|
if args.set_url:
|
|
config.base_url = args.set_url
|
|
config.save()
|
|
print(f"Set Platform URL to: {args.set_url}")
|
|
|
|
if args.set_key:
|
|
config.api_key = args.set_key
|
|
config.save()
|
|
print(f"Set API key: {args.set_key[:8]}...")
|
|
|
|
if not args.set_url and not args.set_key:
|
|
print(f"Platform URL: {config.base_url}")
|
|
print(
|
|
f"API Key: {'****' + config.api_key[-4:] if config.api_key else 'Not set'}"
|
|
)
|
|
print(f"Org ID: {config.org_id or 'Not set'}")
|
|
|
|
sys.exit(0)
|
|
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|