Files
2025-11-29 18:17:27 +08:00

218 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
# /// script
# dependencies = [
# "httpx",
# "click",
# ]
# ///
"""
Web Search Script using Brave Search API
Search the internet using Brave Search API.
Requires BRAVE_API_KEY environment variable.
Usage:
uv run search.py --query "anthropic claude"
uv run search.py --query "python" --count 10
uv run search.py --query "keyword" --json
"""
import json
import os
import sys
import click
import httpx
# Configuration
API_BASE_URL = "https://api.search.brave.com/res/v1"
API_TIMEOUT = 30.0 # seconds
USER_AGENT = "agent-base-web-access/1.0"
class BraveSearchClient:
"""HTTP client for Brave Search API"""
def __init__(self, api_key: str):
"""Initialize Brave Search client
Args:
api_key: Brave Search API key
"""
self.api_key = api_key
self.client = httpx.Client(
base_url=API_BASE_URL,
timeout=API_TIMEOUT,
headers={
"User-Agent": USER_AGENT,
"X-Subscription-Token": api_key,
"Accept": "application/json",
},
)
def __enter__(self) -> "BraveSearchClient":
"""Context manager entry"""
return self
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
"""Context manager exit - cleanup"""
self.client.close()
def search(self, query: str, count: int = 10) -> list[dict]:
"""Search using Brave Search API
Args:
query: Search query
count: Number of results to return (max 20)
Returns:
List of search results with title, url, description
Raises:
Exception if API call fails
"""
try:
params = {
"q": query,
"count": str(min(count, 20)), # Brave API max is 20
}
response = self.client.get("/web/search", params=params)
response.raise_for_status()
data = response.json()
# Extract web results
web_results = data.get("web", {}).get("results", [])
# Format results
results = []
for result in web_results:
results.append(
{
"title": result.get("title", ""),
"url": result.get("url", ""),
"description": result.get("description", ""),
}
)
return results
except httpx.HTTPStatusError as e:
raise Exception(f"API error: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
raise Exception(f"Network error: {str(e)}")
except Exception as e:
raise Exception(f"Unexpected error: {str(e)}")
def format_search_result(result: dict, index: int) -> str:
"""Format a single search result"""
title = result.get("title", "N/A")
url = result.get("url", "N/A")
description = result.get("description", "")
lines = []
lines.append(f"{index}. {title}")
lines.append(f" {url}")
if description:
# Truncate long descriptions
desc = description[:200] + "..." if len(description) > 200 else description
lines.append(f" {desc}")
return "\n".join(lines)
def format_search_results(query: str, results: list[dict]) -> str:
"""Format search results for human-readable output
Args:
query: Search query
results: List of search results
Returns:
Formatted string for display
"""
lines = []
lines.append("\n" + "=" * 60)
lines.append(f"🔍 Web Search Results for '{query}'")
lines.append("=" * 60)
if not results:
lines.append("\nNo results found for your search.")
lines.append("\nTip: Try different keywords.")
else:
lines.append(f"Found {len(results)} results:\n")
for i, result in enumerate(results, 1):
lines.append(format_search_result(result, i))
lines.append("")
lines.append("" * 60)
lines.append("Powered by Brave Search API")
lines.append("=" * 60)
return "\n".join(lines)
@click.command()
@click.option("--query", required=True, help="Search query")
@click.option("--count", default=10, type=int, help="Number of results (max 20)")
@click.option(
"--json",
"output_json",
is_flag=True,
help="Output as JSON instead of human-readable format",
)
def main(query: str, count: int, output_json: bool) -> None:
"""
Search the web using Brave Search API.
Requires BRAVE_API_KEY environment variable.
Examples:
uv run search.py --query "anthropic claude"
uv run search.py --query "python" --count 5 --json
"""
try:
# Get API key from environment
api_key = os.getenv("BRAVE_API_KEY")
if not api_key:
raise ValueError(
"BRAVE_API_KEY environment variable is required. "
"Get a free API key at https://brave.com/search/api/"
)
if not query.strip():
raise ValueError("Query cannot be empty")
# Search via API
with BraveSearchClient(api_key) as client:
results = client.search(query, count)
# Output results
if output_json:
click.echo(json.dumps(results, indent=2))
else:
formatted = format_search_results(query, results)
click.echo(formatted)
sys.exit(0)
except Exception as e:
if output_json:
error_data = {
"success": False,
"error": "runtime_error",
"message": str(e),
}
click.echo(json.dumps(error_data, indent=2))
else:
click.echo(f"❌ Error: {e}", err=True)
sys.exit(1)
if __name__ == "__main__":
main()