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

# Python client

> Submit runs and read receipts from Python with synchronous and asynchronous clients.

Use the Python client to submit a **run** (one task you send to Millwork; the
API calls it an `execution`), follow its progress, and read its receipt. It includes a synchronous `Solver` and an asynchronous `AsyncSolver`.
Both use the same requests and organization API key as the REST API.

## Install

Requires CPython 3.11 to 3.14 and an organization API key.

```bash theme={null}
python -m pip install millwork-solver==0.1.0
```

The package is [millwork-solver on PyPI](https://pypi.org/project/millwork-solver/).
Import it as `millwork_solver`.

## Make a test run

Use your organization API key and `https://api.getmillwork.dev/v1`.
[API basics](/api-reference/overview) explains authentication. This example
submits a test run and reads its receipt without calling a model.

`Solver()` reads `SOLVERAPI_API_KEY` and `SOLVERAPI_BASE_URL` from the
environment. You can also pass them as `api_key` and `base_url`.

```bash theme={null}
export SOLVERAPI_BASE_URL="https://api.getmillwork.dev/v1"
export SOLVERAPI_API_KEY="<organization API key>"
```

```python theme={null}
import time
import uuid

from millwork_solver import Solver

FINAL_STATUSES = {"completed", "failed", "cancelled", "expired"}

with Solver() as client:
    submitted = client.request(
        "postExecutions",
        body={
            "mode": "echo",
            "task": {"objective": "Check the Millwork API lifecycle."},
            "policy": {
                "data_classes": ["public"],
                "budget": {"max_cost_usd": 1, "max_runtime_s": 30},
            },
        },
        idempotency_key=str(uuid.uuid4()),
    )
    execution_id = submitted["execution_id"]
    status = submitted["status"]
    while status not in FINAL_STATUSES:
        time.sleep(1)
        current = client.request(
            "getExecutionsByExecutionId",
            path_parameters={"executionId": execution_id},
        )
        status = current["status"]

    receipt = client.request(
        "getReceiptsByExecutionId",
        path_parameters={"executionId": execution_id},
    )
    print(status, receipt["mode"])
```

`AsyncSolver` has the same calls: `await client.request(...)`, `async for`
over `pages` and `events`, and `async with` or `aclose()` to close.

## What the client does

Both clients expose one `request` call: name the REST operation by its
operation ID from the [API reference](/api-reference/overview) and pass path
parameters, query values, and a body. `pages` walks a list one page at a time;
`events` follows a run's events until a final status.

| Client call                                                         | REST endpoint                              | What it does                                         |
| ------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------- |
| `request("postExecutions", body=..., idempotency_key=...)`          | `POST /v1/executions`                      | Submits a run.                                       |
| `request("getExecutionsByExecutionId", path_parameters=...)`        | `GET /v1/executions/{executionId}`         | Reads the run's status.                              |
| `events(path_parameters=...)`                                       | `GET /v1/executions/{executionId}/events`  | Yields the run's events and stops at a final status. |
| `request("getExecutionsByExecutionIdResult", path_parameters=...)`  | `GET /v1/executions/{executionId}/result`  | Reads the model output of a completed live run.      |
| `request("getReceiptsByExecutionId", path_parameters=...)`          | `GET /v1/receipts/{executionId}`           | Reads the receipt.                                   |
| `request("postExecutionsByExecutionIdCancel", path_parameters=...)` | `POST /v1/executions/{executionId}/cancel` | Cancels a run.                                       |
| `request("getModelCatalog")`                                        | `GET /v1/model-catalog`                    | Lists the models your organization can save now.     |
| `request("postArms", body=...)`                                     | `POST /v1/arms`                            | Saves a model. The API calls a saved model an `arm`. |
| `pages("getArms", item_field="arms")`                               | `GET /v1/arms`                             | Lists saved models, one page per iteration.          |

A **test run** (the API calls it Echo, `mode: "echo"`) reaches a final status
and returns a receipt. It calls no model, so it has no result content and no
platform fee, and reading its result is not part of the test-run flow.

You own the **request key**. Pass it as `idempotency_key` when you submit a run
and the client sends it as the `Idempotency-Key` header. The client never
makes one up for you, and a submit without one is sent once.

A read (`GET` or `HEAD`) that hits a network failure or a `5xx` response is
retried, up to two times by default, with a doubling wait that starts at 500
milliseconds. A write with a request key is retried the same way. **A write
without a request key is never retried by the client.** A `429` is not
retried either; read `retry_after_seconds` and wait. Change the defaults with
`max_retries` and `retry_backoff_ms` on the constructor, or with
`SOLVERAPI_MAX_RETRIES` and `SOLVERAPI_RETRY_BACKOFF_MS` in the environment.

## Errors

Every exception is importable from `millwork_solver`.

| Exception                | When                                                                                                                                                       | Fields                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ProblemError`           | Millwork answered with an error.                                                                                                                           | `code` (the error code, such as `unauthenticated`, `insufficient_credit`, `idempotency_conflict`, `rate_limited`, `upstream_unavailable`, `not_found`), `status`, `title`, `detail`, `instance`, `problem_type`, `retry_after_seconds`, `field_errors` (each with `field` and `message`), `retryable` (true for `5xx`), and the full `problem` object. |
| `NetworkError`           | The request never produced an HTTP response: an unreachable host or a TLS failure. A malformed base URL raises `ValueError` when you construct the client. | `cause`, the underlying exception.                                                                                                                                                                                                                                                                                                                     |
| `ProtocolError`          | The response was not the JSON or error shape the client expects.                                                                                           | Message only.                                                                                                                                                                                                                                                                                                                                          |
| `OperationBoundaryError` | The operation ID is not one the client offers, or its path parameters do not match.                                                                        | Message only.                                                                                                                                                                                                                                                                                                                                          |
| `ClientClosedError`      | A call after `close()` or `aclose()`.                                                                                                                      | Message only.                                                                                                                                                                                                                                                                                                                                          |

An empty list is a normal answer, not an error. For what each error code
means and which are safe to retry, read
[Errors and retries](/guides/errors-and-retries).

<CardGroup cols={2}>
  <Card title="API basics" icon="book" href="/api-reference/overview">
    Authentication, request keys, and the response shapes the client returns.
  </Card>

  <Card title="Make your first API call" icon="terminal" href="/get-started/first-api-call">
    Send a free test run with curl and read its receipt.
  </Card>
</CardGroup>
