Metadata-Version: 2.4
Name: millwork-solver
Version: 0.1.1
Summary: Sync and async Python clients for the Millwork Solver API
Author: Millwork
License-Expression: Apache-2.0
Project-URL: Repository, https://github.com/millworkdev/solver-python
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx==0.28.1
Dynamic: license-file

# Millwork Solver for Python

`millwork-solver` is an early preview client for the Millwork Solver API. Import
`millwork_solver` to use the synchronous `Solver` client or the asynchronous
`AsyncSolver` client on CPython 3.11 through 3.14.

The [generated REST lifecycle reference](https://docs.getmillwork.dev/api-reference/output-checks)
owns output-check operation IDs and fields. The
[verifier-dock contract](https://docs.getmillwork.dev/contracts/verifier-dock/v1.json)
owns the endpoint request and response. Follow
[Recipe 0](https://docs.getmillwork.dev/cookbook/output-checks/build-the-dock)
for local tests, deployment, and a receipt.

Pass `api_key` and `base_url` directly, or use `SOLVERAPI_API_KEY` and
`SOLVERAPI_BASE_URL`. Retry settings use `SOLVERAPI_MAX_RETRIES` (default `2`)
and `SOLVERAPI_RETRY_BACKOFF_MS` (default `500`).

Both clients provide the same typed operation surface, explicit timeout and
close behavior, and receipt-aware responses. Echo reaches a terminal status and
returns a receipt; it does not return result content.

Use the API base URL with its `/v1` suffix:

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

Call an operation by its OpenAPI `operation_id`. This complete test run uses
`postExecutions`; the request must include both `task` and `policy`:

```python
import uuid

from millwork_solver import Solver

with Solver() as client:
    run = 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()),
    )
    receipt = client.request(
        "getReceiptsByExecutionId",
        path_parameters={"executionId": run["execution_id"]},
    )
    print(run["status"], receipt["mode"])
```

Common operation IDs include `getAccount`, `getArms`, `getModelCatalog`,
`getVerifiers`, `postExecutions`, `getExecutionsByExecutionId`,
`getExecutionsByExecutionIdResult`, and `getReceiptsByExecutionId`. The package
ships the complete operation contract in `millwork_solver/_contract.json`.

## Verifier lifecycle

The installed client can register and test a public output check. Set
`VERIFIER_URL` to an endpoint implementing the
[output-check contract](https://docs.getmillwork.dev/cookbook/output-checks/build-the-dock#the-endpoint-contract):

```python
import os
import uuid
from millwork_solver import Solver

with Solver() as client:
    registered = client.request("postVerifiers", body={
        "display_name": "My output check", "version": "1.0.0", "kind": "endpoint",
        "endpoint": {"url": os.environ["VERIFIER_URL"], "auth_ref": ""},
        "input_data_classes": ["public"],
        "scoring": {"correctness": "boolean_anchors", "quality": "scalar_0_1"},
    }, idempotency_key=str(uuid.uuid4()))
    verifier_id = registered["verifier_id"]
    print(verifier_id, client.request("postVerifiersByVerifierIdTest",
          path_parameters={"verifierId": verifier_id}, idempotency_key=str(uuid.uuid4())))
```

The protected-connection methods below describe this source checkout. The
published `millwork-solver==0.1.0` wheel has six public verifier operation
IDs in its catalog but does not include `verifier_connection`. Until a wheel carrying
these methods is published and read back, use the
[private CLI or dashboard path](https://docs.getmillwork.dev/guides/connect-an-output-check#connect-a-protected-endpoint)
for a protected endpoint.

For a protected endpoint, register that endpoint with the same secretless
`postVerifiers` body, then use **its** `verifier_id` for the private handoff.
The REST registration has no `access` field; the protected connection is
established by `verifier_connection`. Give `continue_url` only to the intended
person through your host's private browser handoff. The callback below belongs
to that host and must not log or expose the URL. The endpoint key is never an
argument to these methods.

```python
import uuid
from urllib.parse import urlsplit

def start_protected(client, verifier_id, trusted_app_origin, open_private_url):
    intent = client.verifier_connection.create_intent(
        verifier_id, {"kind": "preset_days", "days": 90},
        idempotency_key=str(uuid.uuid4()))
    expected = urlsplit(trusted_app_origin)
    actual = urlsplit(intent["continue_url"])
    if (actual.scheme, actual.netloc) != (expected.scheme, expected.netloc):
        raise ValueError("Unexpected private-entry origin")
    open_private_url(intent["continue_url"])
    return intent["intent_id"]

def continue_protected(client, verifier_id, operation_key):
    return client.verifier_connection.continue_connection(
        verifier_id, operation_key=operation_key,
        idempotency_key=str(uuid.uuid4()))
```

`enter_key` means private entry is still needed. `resume` means retain the same
operation key and use a fresh request idempotency key. `start_again` means the
endpoint test failed; correct the endpoint or key and create a new intent.
`inspect` means another connection change intervened. `refused` means the server
rejected the change with a non-retryable client error; read `outcome.error["status"]`
and `outcome.error["title"]`, inspect that operation, and correct the cause before
another action. A `401` or `403` raises a `ProblemError` instead of returning a
lifecycle outcome. Only a read-backed `done`
confirms completion. The async `AsyncSolver.verifier_connection` has the same
methods, awaited. Replacement and restoration use a new intent and the same
private-entry path. Disconnect requires explicit caller confirmation:
`client.verifier_connection.disconnect(verifier_id, operation_key=...,
idempotency_key=..., confirmed=True)`. Millwork stopping use of a key does not
revoke it at your endpoint. A governed run requires separate authorization and
uses this `verifier_id`.
