Metadata-Version: 2.3
Name: canonical-search
Version: 0.2.0
Summary: Python SDK for Canonical company search — find companies using natural language
License: MIT
Author: Synaptic
Author-email: hello@synaptic.com
Requires-Python: >=3.11,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Dist: httpx (>=0.27.0,<0.28.0)
Requires-Dist: pydantic (>=2.5.0,<3.0.0)
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
Description-Content-Type: text/markdown

# canonical-search

Typed Python SDK for [Canonical](https://trycanonical.ai) company search — find and enrich companies
using natural language or structured filters. A thin, sync + async client over the public REST API.

## Install

```bash
pip install canonical-search
```

Requires Python 3.11+.

## Quick Start

```python
from canonical_search import CanonicalClient

client = CanonicalClient(api_key="sk_your_key")

# Natural-language search
results = client.search("AI healthcare startups")
for company in results.results:
    print(f"{company.name} — {company.domain}")

# Async twin
results = await client.asearch("B2B fashion tech companies")
```

### Environment-based config

```bash
export CANONICAL_API_KEY=sk_your_key
export CANONICAL_API_BASE_URL=https://trycanonical.ai  # optional
export CANONICAL_TIMEOUT=30.0                          # optional
```

```python
from canonical_search.config import client_from_env

client = client_from_env()
results = client.search("fintech companies in Europe")
```

## Methods

Every method has an `async` twin (`a`-prefixed, e.g. `asearch`, `alookup_companies`).

| Method | Purpose | Credits |
|---|---|---|
| `search(query, top_k=25)` | Natural-language search | 1 per strong result |
| `search_structured(description=None, *, filters=None, intent=None, top_k=25, include_partials=False)` | Search by typed filters | 1 per strong result |
| `find_similar_companies(company_domain, top_k=25, intent=None)` | Look-alikes for a seed domain | 1 per strong result |
| `lookup_companies(names, k=5, disambiguation_mode="auto_when_confident")` | Resolve names → candidate domains | 1 per call when ≥1 candidate |
| `get_company_details(company_domain)` | Full profile + people + relationships | 1 when found (404 free) |
| `get_account_status()` | Credit balance, plan, rate limits | free |

`top_k` is 1–1000. Partial matches (`verdict == "partial"`) are never billed. `domain` is the public
handle — internal numeric ids are never exposed.

### Structured search

```python
from canonical_search import CanonicalClient, StructuredFilters, StructuredLocation

client = CanonicalClient(api_key="sk_your_key")

results = client.search_structured(
    description="AI diagnostic platforms for hospitals",  # optional semantic core
    filters=StructuredFilters(
        location=StructuredLocation(countries=["United States"]),
        employee_count_min=50,
        funding_series=["series_a", "series_b"],
        founding_year_min=2018,
    ),
    intent="sales_prospecting",
    top_k=25,
)
```

`funding_series`: `pre_seed`, `seed`, `series_a`…`series_g_plus`, `growth`, `late`, `bridge`, `venture`,
`angel`, `other`. `intent`: `sales_prospecting`, `sales_timing`, `sales_expansion`, `competitive_tracking`,
`emerging_competitor_scan`, `talent_source`, `recruiter_employer_vet`, `jobseeker_stability`,
`jobseeker_growth`. `founder_prior_categories`: `faang`, `big_tech`, `unicorn`, `top_startup`, `mbb`.

### Company details, lookup, account status

```python
details = client.get_company_details("stripe.com")
print(details.company.name, details.company.employee_count)
for person in details.people:
    print(person.name, "—", person.role)

resp = client.lookup_companies(["stripe", "adyen"])
for name, result in resp.results.items():
    if result.auto_resolve_recommended:
        print(name, "→", result.primary_candidate_domain)

status = client.get_account_status()
print(status.plan, status.credits.total, "credits left")
```

## Error handling

```python
from canonical_search import (
    CanonicalClient,
    AuthenticationError,
    InsufficientCreditsError,
    NotFoundError,
    InvalidRequestError,
    RateLimitError,
    APIError,
)

client = CanonicalClient(api_key="sk_your_key")
try:
    results = client.search("fintech")
except InsufficientCreditsError:
    print("Out of credits — top up at /billing")
except RateLimitError as e:
    print(f"Rate limited — retry after {e.retry_after}s")
```

All exceptions subclass `CanonicalError` and carry `.status_code` and `.detail`:
`AuthenticationError` (401), `InsufficientCreditsError` (402), `NotFoundError` (404),
`InvalidRequestError` (400/422), `RateLimitError` (429, with `.retry_after`), `APIError` (5xx).

## Response schema

```python
SearchResponse:
    results: list[Company]
    count: int
    query: str
    credits_used: int
    credits_remaining: int | None

Company:
    name: str
    website: str
    domain: str                    # public handle
    description: str
    headquarters: str | None
    employee_count: int | None
    founding_year: int | None
    dimensions: dict | None
    funding: FundingSummary | None
    verdict: str | None
    verdict_reason: str | None
```

## Framework integration

The SDK returns Pydantic models (`.model_dump_json()` / `.model_dump()`), so it drops into any
framework that accepts Python functions.

### LangChain

```python
from langchain_core.tools import tool
from canonical_search import CanonicalClient

client = CanonicalClient(api_key="sk_your_key")

@tool
def search_companies(query: str, top_k: int = 25) -> str:
    """Search for companies using natural language."""
    return client.search(query, top_k).model_dump_json()
```

### AutoGen (classic / AG2)

```python
# pip install "ag2[openai]"  — register_function is the v0.2/AG2 API
from autogen import register_function
from canonical_search import CanonicalClient

client = CanonicalClient(api_key="sk_your_key")

def search_companies(query: str, top_k: int = 25) -> str:
    """Search for companies using natural language."""
    return client.search(query, top_k).model_dump_json()

register_function(search_companies, caller=assistant, executor=executor,
                  description="Search for companies using natural language")
```

### CrewAI

```python
from crewai.tools import tool
from canonical_search import CanonicalClient

client = CanonicalClient(api_key="sk_your_key")

@tool("Company Search")
def search_companies(query: str, top_k: int = 25) -> str:
    """Search for companies using natural language."""
    return client.search(query, top_k).model_dump_json()
```

### Agno

```python
from agno.tools import tool
from canonical_search import CanonicalClient

client = CanonicalClient(api_key="sk_your_key")

@tool
def search_companies(query: str, top_k: int = 25) -> str:
    """Search for companies using natural language."""
    return client.search(query, top_k).model_dump_json()
```

## MCP server

Canonical also offers a hosted, OAuth-authenticated **remote MCP server** at
`https://trycanonical.ai/mcp/` (add it as a connector in Claude, Cursor, VS Code, etc.). It is a
separate hosted service — it is **not** part of this pip package, and there is no `[mcp]` extra.
See the [MCP docs](https://trycanonical.ai/documentation/mcp) for connection details.

## Get an API key

1. Sign up at [trycanonical.ai](https://trycanonical.ai)
2. Go to Settings → API Keys
3. Create a new key (starts with `sk_`)

