Skip to content

Commit cd86657

Browse files
author
CutOptim
committed
cutoptim 1.0.0
0 parents  commit cd86657

13 files changed

Lines changed: 1385 additions & 0 deletions

File tree

‎.github/logo.png‎

1.52 KB
Loading

‎.github/workflows/ci.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# CI for the mirror repo cutoptim/engine-python (published from the CutOptim monorepo, sdks/python).
2+
name: ci
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
permissions:
8+
contents: read
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python: ['3.9', '3.11', '3.13']
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python }}
20+
- run: python -m pip install --upgrade pip build
21+
- run: python -m build
22+
- run: pip install dist/*.whl
23+
- name: Import + webhook signature vector
24+
run: |
25+
python - <<'PY'
26+
import json, cutoptim
27+
from cutoptim import verify_webhook
28+
v = json.load(open("tests/webhook-vector.json"))
29+
assert verify_webhook(v["body"], v["header"], v["secret"], now_s=v["t"])
30+
assert not verify_webhook(v["body"] + " ", v["header"], v["secret"], now_s=v["t"])
31+
print("cutoptim", cutoptim.__version__, "webhook vector ok")
32+
PY

‎.github/workflows/release.yml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Publish cutoptim to PyPI on a version tag (v1.2.3), from the mirror repo cutoptim/engine-python.
2+
# PyPI trusted publishing (OIDC, no token) with attestations. The `release` environment holds the approval gate.
3+
name: release
4+
on:
5+
push:
6+
tags: ['v*']
7+
permissions:
8+
contents: read
9+
jobs:
10+
publish:
11+
runs-on: ubuntu-latest
12+
environment: release
13+
permissions:
14+
contents: read
15+
id-token: write
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.12'
21+
- name: The tag must equal the project version
22+
run: |
23+
V=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
24+
test "v$V" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != pyproject $V"; exit 1; }
25+
- run: python -m pip install --upgrade pip build
26+
- run: python -m build
27+
- uses: pypa/gh-action-pypi-publish@release/v1

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__pycache__/
2+
*.pyc
3+
dist/
4+
build/
5+
*.egg-info/

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 CutOptim
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<p align="center">
2+
<a href="https://cutoptim.com/engine"><img src="https://raw.githubusercontent.com/cutoptim/engine-python/main/.github/logo.png" width="88" height="88" alt="CutOptim"></a>
3+
</p>
4+
5+
<h1 align="center">CutOptim Engine API — Python SDK</h1>
6+
7+
<p align="center">
8+
Cutting optimization over HTTPS: <b>2D panels</b>, <b>1D linear</b>, <b>timber</b> and <b>true-shape nesting</b>.<br>
9+
Deterministic plans, saw-ready cut sequences — standard library only.
10+
</p>
11+
12+
<p align="center">
13+
<a href="https://pypi.org/project/cutoptim/"><img alt="PyPI" src="https://img.shields.io/pypi/v/cutoptim?color=f0c040&labelColor=0d0f14&label=pypi"></a>
14+
<a href="https://github.com/cutoptim/engine-python/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/cutoptim/engine-python/actions/workflows/ci.yml/badge.svg"></a>
15+
<img alt="Python 3.9+" src="https://img.shields.io/badge/python-%E2%89%A53.9-f0c040?labelColor=0d0f14">
16+
<img alt="Zero dependencies" src="https://img.shields.io/badge/dependencies-0-f0c040?labelColor=0d0f14">
17+
<a href="https://github.com/cutoptim/engine-python/blob/main/LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-f0c040?labelColor=0d0f14"></a>
18+
</p>
19+
20+
<p align="center">
21+
<a href="https://cutoptim.com/engine/reference/quickstart">Quickstart</a> ·
22+
<a href="https://cutoptim.com/engine/reference">API reference</a> ·
23+
<a href="https://cutoptim.com/engine/reference/guides/webhooks">Webhooks</a> ·
24+
<a href="https://cutoptim.com/engine/reference/changelog">Changelog</a> ·
25+
<a href="https://cutoptim.com/dashboard#api-keys">Get an API key</a>
26+
</p>
27+
28+
---
29+
30+
## Why this SDK
31+
32+
- **Typed from the contract.** Request and response shapes are `TypedDict`s generated from the [OpenAPI 3.1 document](https://cutoptim.com/engine/openapi.json) — mypy and your editor know every field.
33+
- **No dependencies.** `urllib`, `json` and `hmac` from the standard library; Python 3.9+.
34+
- **Retries that never cost twice.** Every optimize call carries an `Idempotency-Key`; a retried call returns the stored answer instead of a second charge.
35+
- **Async jobs without the boilerplate.** `wait_for_job()` polls the `max` engine for you — or let a webhook tell you.
36+
- **Webhook verification built in.** `verify_webhook()` checks the signature on the raw body, with the replay window and secret rotation handled.
37+
38+
## Install
39+
40+
```bash
41+
pip install cutoptim
42+
```
43+
44+
A free **test key** (`co_test_…`, never billed) is enough to build the whole integration — create one on your [dashboard](https://cutoptim.com/dashboard#api-keys).
45+
46+
## Quickstart
47+
48+
```python
49+
import os
50+
from cutoptim import CutOptim, is_job_accepted
51+
52+
client = CutOptim(api_key=os.environ["CUTOPTIM_KEY"])
53+
54+
plan = client.optimize_2d({
55+
"parts": [
56+
{"name": "Side", "w": 720, "h": 560, "qty": 2},
57+
{"name": "Shelf", "w": 764, "h": 540, "qty": 3, "rotatable": False},
58+
],
59+
"stock": [{"w": 2800, "h": 2070, "price": 48}],
60+
"options": {"kerf": 4},
61+
})
62+
63+
if not is_job_accepted(plan):
64+
print(f"{plan['metrics']['sheetCount']} sheet(s), {plan['metrics']['yieldPct']}% yield")
65+
for sheet in plan["sheets"]:
66+
for p in sheet["parts"]:
67+
print(p["name"], p["x"], p["y"], p["w"], p["h"], "rotated" if p["rotated"] else "")
68+
```
69+
70+
The same input always returns the same plan — cache it, test against it, quote from it.
71+
72+
## Every mode
73+
74+
```python
75+
# 1D linear — bars, profiles, tubes
76+
bars = client.optimize_1d({"parts": [{"name": "Rail", "length": 1200, "qty": 6}], "stock": [{"length": 6000}]})
77+
print(bars["metrics"]["rodCount"], bars["metrics"]["yieldPct"])
78+
79+
# Timber — lengths cut from beams of a given cross-section (sw × sh)
80+
timber = client.optimize_wood({
81+
"parts": [{"name": "Joist", "sw": 45, "sh": 145, "length": 3600, "qty": 4}],
82+
"stock": [{"sw": 45, "sh": 145, "length": 4800}],
83+
})
84+
85+
# True-shape nesting — polygons (or SVG/DXF sources) on sheets
86+
nest = client.optimize_nest({
87+
"parts": [{"name": "Bracket", "polygon": [[0, 0], [120, 0], [120, 40], [40, 120], [0, 120]], "qty": 10}],
88+
"stock": [{"w": 1000, "h": 500}],
89+
})
90+
if not is_job_accepted(nest):
91+
print(nest["metrics"]["sheetCount"], nest["metrics"]["density"])
92+
```
93+
94+
Check a request without spending a call — the validate endpoints need no key and give the same verdict:
95+
96+
```python
97+
CutOptim().validate_2d(request) # {"valid": True, "warnings": [...]} or CutOptimError(400)
98+
```
99+
100+
## Async `max` jobs
101+
102+
`"engine": "max"` searches harder (seconds to a minute) and answers `202` with a job id instead of a plan:
103+
104+
```python
105+
accepted = client.optimize_2d({**request, "engine": "max"})
106+
if is_job_accepted(accepted):
107+
job = client.wait_for_job(accepted["jobId"], timeout_s=300) # polls GET /v1/jobs/{id}, no quota
108+
if job["status"] == "succeeded":
109+
print(job["result"])
110+
```
111+
112+
## Webhooks
113+
114+
Instead of polling, register an endpoint on your [dashboard](https://cutoptim.com/dashboard#api-webhooks) and receive a signed `POST` (`job.succeeded` / `job.failed`) when a `max` job finishes. Verify it on the **raw** body:
115+
116+
```python
117+
# Flask
118+
from flask import Flask, request
119+
from cutoptim import verify_webhook
120+
121+
@app.post("/cutoptim/webhook")
122+
def cutoptim_webhook():
123+
if not verify_webhook(request.get_data(), request.headers.get("CutOptim-Signature"), WEBHOOK_SECRET):
124+
return "", 400
125+
event = request.get_json() # deduplicate on event["id"] — delivery is at least once
126+
return "", 204 # answer within 10 s, then do the work
127+
```
128+
129+
```python
130+
# FastAPI
131+
from fastapi import FastAPI, Request, Response
132+
133+
@app.post("/cutoptim/webhook")
134+
async def cutoptim_webhook(request: Request):
135+
raw = await request.body()
136+
if not verify_webhook(raw, request.headers.get("CutOptim-Signature"), WEBHOOK_SECRET):
137+
return Response(status_code=400)
138+
return Response(status_code=204)
139+
```
140+
141+
During a secret rotation both secrets sign for 24 hours — pass both: `verify_webhook(raw, header, [new_secret, old_secret])`.
142+
143+
## Retries and idempotency
144+
145+
| Situation | What the client does |
146+
|---|---|
147+
| `429 busy`, `503`, network error | retries (2× by default), waiting `Retry-After` when the API sends it |
148+
| `409 idempotency_in_progress` | retries — the first request is still running; the retry receives its answer |
149+
| any optimize call | sends an `Idempotency-Key` (a fresh UUID, or yours) — the **same** key on every retry |
150+
151+
Pass your own key to make retries safe across process restarts too:
152+
153+
```python
154+
client.optimize_nest(request, idempotency_key=f"order-{order_id}")
155+
```
156+
157+
Within 24 hours the same key and request return the stored answer — not charged again, and a `max` job is not queued twice.
158+
159+
## Errors
160+
161+
Any non-2xx answer raises `CutOptimError`:
162+
163+
```python
164+
from cutoptim import CutOptimError
165+
166+
try:
167+
client.optimize_2d(request)
168+
except CutOptimError as e:
169+
print(e.status, e.code, e.details, e.retry_after) # branch on e.code, never on the message
170+
```
171+
172+
| `code` | Status | Meaning |
173+
|---|---|---|
174+
| `invalid_request` | 400 | the request is wrong — `details["path"]` names the field; never charged |
175+
| `unauthorized` | 401 | missing or unknown API key |
176+
| `quota_exceeded` | 402 | the account's monthly quota is used up — `retry_after` counts to the reset |
177+
| `key_revoked` | 403 | the key is revoked, or the API subscription is not active |
178+
| `idempotency_conflict` | 409 | the key was used for a different request — use a new key |
179+
| `too_large` | 413 | an input cap was exceeded (parts, rows, body size) |
180+
| `busy` | 429 | at capacity right now — retried automatically |
181+
| `service_unavailable` | 503 | an engine is not available on this deployment |
182+
| `solve_timeout` | 504 | the nesting solve ran out of time |
183+
| `timeout` | — | `wait_for_job()` gave up waiting (the job keeps running) |
184+
185+
A network failure that outlasts the retries is raised as the underlying `urllib.error.URLError`. The full list is on the [errors page](https://cutoptim.com/engine/reference/errors).
186+
187+
## Configuration
188+
189+
```python
190+
CutOptim(
191+
api_key="co_live_…", # not needed for health() and validate_*()
192+
base_url="https://api.cutoptim.com", # default
193+
max_retries=2, # 429 / 503 / 409 in-progress / network errors
194+
timeout_s=30.0, # per request
195+
)
196+
```
197+
198+
## API coverage
199+
200+
| Method | Endpoint | Quota |
201+
|---|---|---|
202+
| `optimize_2d(req, idempotency_key=None)` | `POST /v1/optimize/2d` | 1 call |
203+
| `optimize_1d(req, idempotency_key=None)` | `POST /v1/optimize/1d` | 1 call |
204+
| `optimize_wood(req, idempotency_key=None)` | `POST /v1/optimize/wood` | 1 call |
205+
| `optimize_nest(req, idempotency_key=None)` | `POST /v1/optimize/nest` | 1 call |
206+
| `validate_2d` · `validate_1d` · `validate_wood` · `validate_nest` | `POST /v1/validate/*` | free, no key |
207+
| `import_nest(req)` | `POST /v1/import/nest` | free (key required) |
208+
| `get_job(id)` · `wait_for_job(id)` | `GET /v1/jobs/{id}` | free |
209+
| `usage()` | `GET /v1/usage` | free |
210+
| `health()` | `GET /v1/health` | free, no key |
211+
212+
## Versioning
213+
214+
The SDK follows semver. The API is `v1` and only grows within it: a new field, endpoint or option arrives as a **minor** SDK release with the regenerated types; nothing you use today is removed or narrowed. See [versioning and stability](https://cutoptim.com/engine/reference/versioning) and the [API changelog](https://cutoptim.com/engine/reference/changelog) (also as RSS).
215+
216+
## Support
217+
218+
- Questions and bugs: [GitHub issues](https://github.com/cutoptim/engine-python/issues) or [support@cutoptim.com](mailto:support@cutoptim.com)
219+
- Limits and quotas: [cutoptim.com/engine/reference/limits](https://cutoptim.com/engine/reference/limits)
220+
221+
This repository is published from the CutOptim monorepo; pull requests are welcome and applied upstream.
222+
223+
## License
224+
225+
[MIT](https://github.com/cutoptim/engine-python/blob/main/LICENSE) © CutOptim

‎cutoptim/__init__.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""cutoptim — the official client for the CutOptim Engine API (https://cutoptim.com/engine/reference).
2+
3+
Standard library only (urllib, hmac). Types are generated from the OpenAPI document (types_gen.py).
4+
5+
from cutoptim import CutOptim
6+
client = CutOptim(api_key="co_test_...")
7+
plan = client.optimize_2d({"parts": [{"w": 600, "h": 400, "qty": 4}], "stock": [{"w": 2440, "h": 1220}]})
8+
"""
9+
from .client import CutOptim, CutOptimError, is_job_accepted
10+
from .webhooks import verify_webhook, SIGNATURE_HEADER
11+
12+
__version__ = "1.0.0"
13+
__all__ = ["CutOptim", "CutOptimError", "is_job_accepted", "verify_webhook", "SIGNATURE_HEADER", "__version__"]

0 commit comments

Comments
 (0)