Metadata-Version: 2.3
Name: onepot
Version: 0.2.0
Summary: Python client for the onepot API
Requires-Dist: httpx==0.28.1
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# onepot-python

Python client for the [onepot](https://onepot.ai) API — find purchasable analogs of your query molecules, with optional retrosynthesis decomposition and per-position building-block filtering.

## Installation

```bash
uv add onepot
# or
pip install onepot
```

## Quick start

```python
from onepot import Client

client = Client(api_key="your-api-key")

resp = client.search(
    smiles_list=["c1ccc(NC(=O)c2ccccc2)cc1"],
    max_results=10,
)
for r in resp["queries"][0]["results"]:
    print(r["smiles"], r["similarity"], r["price_usd"])
```

## Features

- **Similarity search** — Tanimoto nearest analogs from the onepot catalog
- **Substructure search** — purchasable molecules containing a SMILES/SMARTS pattern
- **Decomposition + BB filters** — inspect the retro paths the system considered for your query, then refine which candidate BBs are eligible per position
- **Risk and price filters** — exclude results above a chemistry-risk, supplier-risk, or price threshold
- **Streaming** — single-molecule searches with SSE progress updates
- **Ordering** — submit results for synthesis quoting

## Search

### Basic

```python
resp = client.search(smiles_list=["c1ccc(-c2ccccc2)cc1"], max_results=10)
```

```bash
curl -X POST https://api.onepot.ai/v1/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"smiles_list": ["c1ccc(-c2ccccc2)cc1"], "max_results": 10}'
```

### Streaming

For single-molecule searches with real-time progress events. Status lifecycle: `starting` → `synthesis` → `rescoring` → `complete`. The final event includes a `results` list with the same fields as the batch endpoint.

```python
for event in client.search_stream("c1ccc(NC(=O)c2ccccc2)cc1", max_results=10):
    print(event["status"], event["message"])
    if event["status"] == "complete":
        results = event["results"]
```

```bash
curl -sN -X POST https://api.onepot.ai/v1/search/stream \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"smiles": "c1ccc(NC(=O)c2ccccc2)cc1", "max_results": 5}'
```

### Substructure search

Pass `substructure_search=True` to return purchasable molecules that contain the query as a substructure, instead of similarity hits. The query can be a SMILES or a SMARTS pattern.

```python
resp = client.search(
    smiles_list=["c1ccc(C(=O)N)cc1"],
    max_results=10,
    substructure_search=True,
)
```

```bash
curl -X POST https://api.onepot.ai/v1/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"smiles_list": ["c1ccc(C(=O)N)cc1"], "max_results": 10, "substructure_search": true}'
```

### Risk and price filters

All optional. When set, results that exceed the threshold are excluded.

| Parameter | Type | Values |
|-----------|------|--------|
| `max_price` | int | USD, e.g. `200`, `500` |
| `max_supplier_risk` | string | `"low"`, `"medium"`, `"high"` |
| `max_chemistry_risk` | string | `"low"`, `"medium"`, `"high"` |

Setting `max_chemistry_risk` automatically includes the `chemistry_risk` field in the response. Pass `include_chemistry_risk_score=True` for the raw probability score.

```python
resp = client.search(
    smiles_list=["c1ccc(-c2ccccc2)cc1"],
    max_results=10,
    max_price=500,
    max_supplier_risk="medium",
    max_chemistry_risk="low",
    include_chemistry_risk_score=True,
)
```

### Decompose & bb_filters

Use `decompose=True` to receive the retrosynthetic paths the system considered for each query — every `reaction_class` it found and the BB SMILES of your query at each position. Then call back with `bb_filters` to constrain which candidate BBs are eligible per position. Every enumerated result is automatically tagged with the `reaction_class` it was made from and the `bbs` that built it.

**Call 1 — discover.**

```python
resp = client.search(
    smiles_list=["c1ccc(NC(=O)c2ccccc2)cc1"],
    max_results=5,
    decompose=True,
)
decompositions = resp["queries"][0]["decompositions"]
rxn = decompositions[0]["reaction_class"]   # e.g. "rxn_5e820be4"
```

**Call 2 — refine.** Force the building block at position 1 to vary (Tanimoto ≤ 0.7 to the query's position-1 BB) while leaving position 0 free.

```python
resp = client.search(
    smiles_list=["c1ccc(NC(=O)c2ccccc2)cc1"],
    max_results=10,
    bb_filters=[{"reaction_class": rxn, "bb_index": 1, "max_similarity": 0.7}],
)
for r in resp["queries"][0]["results"]:
    if r.get("reaction_class") == rxn:
        bb_smiles = [b["smiles"] for b in r["bbs"]]
        print(r["smiles"], "←", " + ".join(bb_smiles))
```

```bash
curl -X POST https://api.onepot.ai/v1/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
    "smiles_list": ["c1ccc(NC(=O)c2ccccc2)cc1"],
    "max_results": 10,
    "bb_filters": [
      {"reaction_class": "rxn_<from-call-1>", "bb_index": 1, "max_similarity": 0.7}
    ]
  }'
```

`reaction_class` values like `"rxn_5e820be4"` come from a prior `decompose=True` response and are stable across calls — pass them through as strings. Each `bb_filters` entry takes optional `min_similarity` and `max_similarity` (Tanimoto, 0.0–1.0); omit a bound to leave that side open. Combine multiple entries to constrain multiple positions in one call. `bb_index` is the 0-based position of the building block within the reaction, matching the ordering in the `bbs` field of a decomposition or annotated result. Unknown `reaction_class` or `min_similarity > max_similarity` is rejected as 422. Streaming searches accept the same parameters via `client.search_stream(...)`.

When a retro decomposition produces multiple paths under the same `reaction_class`, filters apply to each path's candidates independently (similarity is measured against that path's BB SMILES, so the same SMILES can pass one path's filter and fail another's).

### Response shape

```python
{
    "queries": [
        {
            "query_smiles": "c1ccc(NC(=O)c2ccccc2)cc1",
            "query_inchikey": "...",
            "results": [
                {
                    "smiles": "...",
                    "inchikey": "...",
                    "similarity": 0.92,
                    "price_usd": 590,
                    "supplier_risk": "low",
                    "chemistry_risk": "medium",       # if include_chemistry_risk=True
                    "chemistry_risk_score": 0.5,      # if include_chemistry_risk_score=True
                    # present on enumerated results (synthesized analogs):
                    "reaction_class": "rxn_5e820be4",
                    "bbs": [
                        {"bb_index": 0, "smiles": "<bb-smiles>"},
                        {"bb_index": 1, "smiles": "<bb-smiles>"},
                    ],
                },
                ...
            ],
            # if decompose=True:
            "decompositions": [
                {
                    "reaction_class": "rxn_5e820be4",
                    "bbs": [
                        {"bb_index": 0, "smiles": "<bb-smiles>"},
                        {"bb_index": 1, "smiles": "<bb-smiles>"},
                    ],
                },
                ...
            ],
        },
        ...
    ],
    "credits_used": 10,
    "credits_remaining": 990,
}
```

## Order

Submit results for synthesis quoting. Returns an `order_id` you can reference in followup.

```python
order = client.order(
    smiles=["CCO", "c1ccccc1"],
    email="you@example.com",
    notes="Optional notes",
)
# {"order_id": "a1b2c3d4-...", "molecule_count": 2}
```

```bash
curl -X POST https://api.onepot.ai/v1/order \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
    "smiles": ["CCO", "c1ccccc1"],
    "email": "you@example.com",
    "notes": "Optional notes"
  }'
```

## Pricing

Credits are charged per SMILES in the query. Only the chemistry-risk tier affects pricing:

| Option | Credits per SMILES |
|--------|-------------------|
| Base search | 1 |
| `include_chemistry_risk=True` | 5 |
| `include_chemistry_risk_score=True` | 10 |

`decompose`, `bb_filters`, and `substructure_search` don't change the price.
