Async Python library for querying multiple search providers in parallel
Project description
search-cli (Python)
Python port of search-cli. Async library for querying multiple search providers in parallel with result deduplication.
Requirements
- Python 3.11+
Installation
pip install -e .
# With dev dependencies (pytest, respx)
pip install -e ".[dev]"
Configuration
Set API keys as environment variables. Only providers with configured keys are used.
| Variable | Provider |
|---|---|
BRAVE_API_KEY |
Brave Search |
SERPER_API_KEY |
Serper (Google) |
EXA_API_KEY |
Exa (neural search) |
JINA_API_KEY |
Jina |
TAVILY_API_KEY |
Tavily |
PERPLEXITY_API_KEY |
Perplexity |
Usage
import asyncio
from search_cli import search, Mode
async def main():
# General web search
response = await search("python asyncio tutorial")
for r in response.results:
print(r.title, r.url)
# News search
response = await search("AI news", mode=Mode.NEWS, count=10)
# Deep search (maximum coverage)
response = await search("quantum computing", mode=Mode.DEEP)
# JSON output
print(response.model_dump_json(indent=2))
asyncio.run(main())
Modes
| Mode | Providers |
|---|---|
Mode.GENERAL |
brave, serper, exa, jina, tavily, perplexity |
Mode.NEWS |
brave, serper, tavily, perplexity |
Mode.DEEP |
brave, exa, serper, tavily, perplexity |
Advanced: Manage context lifecycle
from search_cli import search, Mode, SearchContext, AppConfig
async def main():
# Reuse a single httpx client across multiple searches
async with SearchContext(config=AppConfig()) as ctx:
r1 = await search("query one", ctx=ctx)
r2 = await search("query two", mode=Mode.NEWS, ctx=ctx)
Public API
| Symbol | Type | Description |
|---|---|---|
search() |
async function |
Main entry point — query, mode, count, opts, ctx |
Mode |
Enum |
GENERAL, NEWS, DEEP |
SearchResult |
BaseModel |
title, url, snippet, source, published?, image_url?, extra? |
SearchResponse |
BaseModel |
version, status, query, mode, results, metadata |
SearchOpts |
BaseModel |
include_domains, exclude_domains, freshness |
AppConfig |
BaseSettings |
keys (ApiKeys), timeout (10s), count (5) |
ApiKeys |
BaseSettings |
6 provider API keys, loaded from env vars |
SearchContext |
class |
Async context manager holding httpx client + config |
SearchError |
Exception |
Base exception with code, message, suggestion |
Architecture
src/search_cli/
├── __init__.py # Public exports
├── enums.py # Mode enum
├── models.py # Pydantic models (SearchResult, SearchResponse, etc.)
├── config.py # ApiKeys + AppConfig (pydantic-settings, env vars)
├── context.py # SearchContext (httpx.AsyncClient lifecycle)
├── engine.py # Parallel search orchestration, URL dedup
├── errors.py # SearchError hierarchy
└── providers/
├── __init__.py # Registry: PROVIDERS_FOR_MODE, build_providers()
├── base.py # BaseProvider ABC (resolve_key, retry_request)
├── brave.py # Brave Search API
├── serper.py # Serper (Google Search) API
├── exa.py # Exa neural search API
├── jina.py # Jina search API
├── tavily.py # Tavily search API
└── perplexity.py # Perplexity chat completions API
How it works
search()receives a query + modeproviders_for_mode()selects providers mapped to that mode, filtered to those with configured API keysasyncio.TaskGrouplaunches all provider searches in parallel, each with per-provider timeout- Individual provider failures are caught and recorded (other providers continue)
- Results are deduplicated by normalized URL (first occurrence wins)
SearchResponseis returned with results + metadata (timing, provider stats)
Testing
pytest -v
23 tests covering models, engine, and all 6 providers. All tests use respx for HTTP mocking — no real API keys required.
Dependencies
| Package | Purpose |
|---|---|
httpx |
Async HTTP client (maps to Rust reqwest) |
pydantic |
Data models + JSON serialization (maps to Rust serde) |
pydantic-settings |
Config from env vars (maps to Rust figment) |
Dev only
| Package | Purpose |
|---|---|
pytest |
Test runner |
pytest-asyncio |
Async test support |
respx |
httpx request mocking |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_multi_agent_search-0.1.0.tar.gz.
File metadata
- Download URL: py_multi_agent_search-0.1.0.tar.gz
- Upload date:
- Size: 10.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06cc2c4c06c8e335453c4720e421280f77199e138992fa710099c4ead45d6116
|
|
| MD5 |
b4b23481aade48604d350bb9967a81fa
|
|
| BLAKE2b-256 |
ef150ecc004d619f5c6e9436995e258db91873f621ca90e40f986002d6a7db5f
|
File details
Details for the file py_multi_agent_search-0.1.0-py3-none-any.whl.
File metadata
- Download URL: py_multi_agent_search-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f483e4619518682b8f62240a66acff2517c8b74fcba62c0ba6eb1a5dd8763110
|
|
| MD5 |
163ef7f61bb7212a0755e3c26463d053
|
|
| BLAKE2b-256 |
7d02350b27f5e743885c875ec2f3e938a4fb21fa45c8c265ab6a153f0f5f27dd
|