> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trycaesar.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Typed Python client for the Caesar API with sync and async support, automatic retries, and Pydantic v2 models.

```python theme={"system"}
# pip install caesar-search    (or: uv add caesar-search)
from caesar_search import Caesar

client = Caesar()  # reads CAESAR_API_KEY
results = client.search("rust async runtime comparison", max_results=5)
doc_id = results.results[0].doc_id
doc = client.read(doc_id, query="which runtime is fastest")
print(doc.content.text if doc.content else "")
```

[Sign up in the Caesar app](https://app.trycaesar.com), create an API key, and set `CAESAR_API_KEY` before running the SDK.

## Install

```bash theme={"system"}
pip install caesar-search
# or
uv add caesar-search
```

The package installs as `caesar-search` and imports as `caesar_search`. Requires Python 3.10+ with `httpx` and Pydantic v2 (installed automatically). Current version: 0.2.0, MIT licensed.

## Clients

`Caesar` is the synchronous client. `AsyncCaesar` has the identical surface with `await`.

```python theme={"system"}
Caesar(*, api_key=None, base_url=None, timeout=30.0, max_retries=3, http_client=None)
AsyncCaesar(*, api_key=None, base_url=None, timeout=30.0, max_retries=3, http_client=None)
```

| Option        | Environment variable | Default                           | Notes                                               |
| ------------- | -------------------- | --------------------------------- | --------------------------------------------------- |
| `api_key`     | `CAESAR_API_KEY`     | required                          | sent as a bearer token                              |
| `base_url`    | `CAESAR_BASE_URL`    | `https://alpha.api.trycaesar.com` | trailing slashes are stripped                       |
| `timeout`     | —                    | `30.0`                            | per-request timeout in seconds (float)              |
| `max_retries` | —                    | `3`                               | retries on 429/5xx; `0` disables                    |
| `http_client` | —                    | —                                 | bring your own `httpx.Client` / `httpx.AsyncClient` |

Both clients are context managers; outside a `with` block, call `close()` (sync) or `aclose()` (async).

```python theme={"system"}
from caesar_search import AsyncCaesar

async with AsyncCaesar() as client:
    results = await client.search("postgres 17 logical replication failover")
```

## Methods

The core methods map to `POST /v1/search`, `POST /v1/document`, `POST /v1/feedback`, plus the [Files knowledge base](/concepts/files) endpoints. Full request and response schemas are in the [API reference](/api-reference/index).

```python theme={"system"}
def search(self, query: str, *, max_results=None, session_id=None,
           verbosity=None, max_chars_total=None, extra_body=None) -> SearchResponse

def read(self, target: str | None = None, *, doc_id=None, url=None, query=None,
         max_chars=None, start_char=None, include=None,
         extra_body=None) -> DocumentResponse

def feedback(self, event_type: str, *, search_id=None, doc_id=None, passage_id=None,
             query=None, rank=None, notes=None, extra_body=None) -> FeedbackResponse

# Files knowledge base
def upload_file(self, file: str | PathLike | bytes, *, filename=None,
                content_type=None, index=True) -> UploadResult
def presign_upload(self, filename: str, size: int, *, content_type=None) -> FilePresignResponse
def list_files(self) -> FileListResponse
def delete_file(self, name: str) -> FileDeleteResponse
def index_files(self, *, mode="incremental") -> FileIndexResponse
def file_index_status(self, sync_id: str) -> FileIndexStatusResponse
```

### Uploading files

`upload_file()` collapses the presign → upload → index flow into one call. Pass a path (the filename defaults to its basename) or bytes with `filename=`:

```python theme={"system"}
upload = client.upload_file("q3-report.pdf")
status = client.file_index_status(upload.sync_id)  # poll until "completed"

hits = client.search(
    "q3 revenue",
    extra_body={"scope": {"indexes": ["workspace"], "workspace_id": "<your-org-id>"}},
)
```

The file bytes go straight to storage via the presigned URL — the API key is never sent there. Batch several uploads with `index=False`, then call `index_files()` once. See [Files](/concepts/files) for supported types, limits, and the workspace search scope.

### How read() picks doc\_id vs URL

The positional `target` is routed by shape: a UUID-shaped string is sent as `doc_id`; anything else is sent as `canonical_url`. Explicit `doc_id=` or `url=` keywords win when given. With neither, the SDK raises `ValueError("provide a doc_id or a url")`.

Defaults: `include` is `["metadata", "content"]`; content selection is `full_document`; content format is markdown; `max_chars` is omitted unless you pass it. See [documents](/concepts/documents) for the response shape.

### Continuation reads

When `content.truncated` is true, resume from where the previous read stopped:

```python theme={"system"}
from caesar_search import Caesar

client = Caesar()
url = "https://www.postgresql.org/docs/17/logical-replication.html"

doc = client.read(url, max_chars=8000)
text = doc.content.text

if doc.content.truncated:
    more = client.read(url, start_char=(doc.content.start_char or 0) + doc.content.char_count)
    text += more.content.text
```

<Warning>
  A non-zero `start_char` forces `full_document` selection so offsets stay contiguous against the raw document text. Combining `start_char` with `query` will not produce query-relevant selection.
</Warning>

### Response shaping

`search()` exposes the [response shaping](/concepts/response-shaping) controls directly:

```python theme={"system"}
results = client.search("rust async runtime comparison",
                        verbosity="compact", max_chars_total=4000)
```

`verbosity` is one of `ids_only`, `compact`, `standard` (the default), or `full` — only `full` includes [provenance](/concepts/provenance). On the wire these become `response.verbosity` and `response.budget.max_chars_total`.

## Errors

All six error classes are importable from `caesar_search`. The hierarchy:

| Class                 | Raised when                                              | Attributes                                                      |
| --------------------- | -------------------------------------------------------- | --------------------------------------------------------------- |
| `CaesarError`         | base class for everything below                          | —                                                               |
| `APIConnectionError`  | the API could not be reached                             | —                                                               |
| `APITimeoutError`     | the request timed out (subclass of `APIConnectionError`) | —                                                               |
| `APIStatusError`      | any non-2xx response                                     | `.status_code`, `.code`, `.message`, `.request_id`, `.response` |
| `AuthenticationError` | HTTP 401 or 403 (subclass of `APIStatusError`)           | as `APIStatusError`                                             |
| `RateLimitError`      | HTTP 429 (subclass of `APIStatusError`)                  | as `APIStatusError`                                             |

`.code` is the stable machine-readable code from the [error envelope](/concepts/errors); the exception message is formatted as `code: message`.

```python theme={"system"}
from caesar_search import Caesar, AuthenticationError, RateLimitError, APIStatusError

client = Caesar()
try:
    results = client.search("postgres 17 logical replication failover")
except AuthenticationError:
    print("check CAESAR_API_KEY")          # 401 or 403
except RateLimitError as e:
    print("rate limited", e.request_id)    # 429, after retries are exhausted
except APIStatusError as e:
    print(e.status_code, e.code, e.request_id)
```

### Retries

The client retries statuses 429, 500, 502, 503, and 504 — up to `max_retries` times (default 3, so 4 attempts total) with exponential backoff starting at 0.5 s and capped at 8 s. Caesar rate limits use `X-RateLimit-Reset`; if a numeric `Retry-After` header is present, the client honors it, also capped at 8 s. HTTP-date `Retry-After` values fall back to the exponential schedule. Timeouts and connection failures are never retried — they raise `APITimeoutError` / `APIConnectionError` immediately. After retries are exhausted, the status error for the last response is raised.

## Raw responses and extra\_body

`client.with_raw_response` mirrors all three methods with the same parameters but returns the raw `httpx.Response` (no model validation) — useful for headers like the [rate-limit headers](/rate-limits):

```python theme={"system"}
raw = client.with_raw_response.search("rust async runtime comparison")
print(raw.status_code, raw.headers["X-RateLimit-Remaining"])
```

`extra_body` merges a dict into the request body last, so it can set fields the typed signature does not expose — and it overrides anything the SDK would have set:

```python theme={"system"}
client.search("rust async runtime comparison",
              extra_body={"response": {"budget": {"max_chars_total": 4000,
                                                  "on_exceed": "error"}}})
```

## Typing

Responses are Pydantic v2 models from `caesar_search.models` (`SearchResponse`, `DocumentResponse`, `FeedbackResponse`, and their nested types). The package ships `py.typed`, so type checkers pick everything up. Field names match the wire format exactly (`search_id`, `doc_id`, `canonical_url`); document metadata lives under `DocumentResponse.doc`, and markdown content lives under `DocumentResponse.content`.

## For agents

* `timeout` is in seconds (`30.0`), not milliseconds. The [TypeScript SDK](/clients/sdks/typescript) uses `timeoutMs` in milliseconds — do not carry values between them unconverted.
* `read()` routes its positional argument purely by UUID shape: UUID goes as `doc_id`, everything else as `canonical_url`. Pass `doc_id=` or `url=` explicitly when ambiguity matters.
* A non-zero `start_char` forces `full_document` selection; pairing it with `query` will not return query-relevant content.
* `extra_body` is merged last and overrides any field the SDK builds, including the `response` wrapper produced by `verbosity`/`max_chars_total`.
* `Caesar()` reads `CAESAR_API_KEY` by default. If no key is provided, API calls fail with `401 missing_api_key`; fail setup early when `CAESAR_API_KEY` is unset.
* Caesar rate limits use `X-RateLimit-Reset`; `Retry-After` is parsed as numeric seconds only when present. HTTP-date values silently fall back to exponential backoff, and timeouts are never retried.
