ddgs / src /ddgs_api /api.py
dromerosm's picture
Add browser UI for API testing
1cb0a2e
import os
from pathlib import Path
from typing import Optional
from fastapi import Depends, FastAPI, Header, HTTPException, status
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from ddgs_api.config import ensure_ca_bundle, env_bool, env_int, env_str, load_env
from ddgs_api.search import ddgs_search
from ddgs_api.ui import render_homepage
def require_auth(authorization: Optional[str] = Header(default=None)) -> None:
expected = os.getenv("API_BEARER_TOKEN")
if not expected:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="API_BEARER_TOKEN is not configured",
)
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
token = authorization.split(" ", 1)[1]
if token != expected:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
class SearchRequest(BaseModel):
query: str = Field(..., min_length=1)
region: str = Field(default_factory=lambda: env_str("DDGS_REGION", "us-en") or "us-en")
safesearch: str = Field(
default_factory=lambda: env_str("DDGS_SAFESEARCH", "moderate") or "moderate"
)
timelimit: Optional[str] = Field(default_factory=lambda: env_str("DDGS_TIMELIMIT", None))
max_results: int = Field(default_factory=lambda: env_int("DDGS_MAX_RESULTS", 10) or 10, ge=1)
backend: Optional[str] = Field(default_factory=lambda: env_str("DDGS_BACKEND", "auto"))
proxy: Optional[str] = Field(default_factory=lambda: env_str("DDGS_PROXY", None))
timeout: int = Field(default_factory=lambda: env_int("DDGS_TIMEOUT", 30) or 30, ge=1)
verify: bool = Field(default_factory=lambda: env_bool("DDGS_VERIFY", True))
def create_app(*, project_root: Path | None = None) -> FastAPI:
load_env(project_root=project_root)
ensure_ca_bundle(project_root=project_root)
app = FastAPI(title="DDGS Search API", version="0.1.0")
ui_defaults = {
"region": env_str("DDGS_REGION", "us-en") or "us-en",
"safesearch": env_str("DDGS_SAFESEARCH", "moderate") or "moderate",
"timelimit": env_str("DDGS_TIMELIMIT", None),
"max_results": env_int("DDGS_MAX_RESULTS", 10) or 10,
"backend": env_str("DDGS_BACKEND", "auto") or "auto",
"timeout": env_int("DDGS_TIMEOUT", 30) or 30,
"verify": env_bool("DDGS_VERIFY", True),
}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/", response_class=HTMLResponse)
def root() -> str:
return render_homepage(ui_defaults)
@app.post("/search")
async def search(request: SearchRequest, _: None = Depends(require_auth)) -> dict[str, object]:
results = ddgs_search(
request.query,
region=request.region,
safesearch=request.safesearch,
timelimit=request.timelimit,
max_results=request.max_results,
backend=request.backend,
proxy=request.proxy,
timeout=request.timeout,
verify=request.verify,
)
return {"query": request.query, "count": len(results), "results": results}
return app
app = create_app()