Async resources
Simulations, sweeps, and dynamic simulations are asynchronous
resources: create(...) returns immediately with status: "queued", and
the work runs server-side in Celery workers. To get the result you can
either poll or subscribe to webhooks.
Pattern 1: poll with wait()
The SDK ships a wait() helper on simulations, sweeps, and dynamic
simulations. It polls retrieve() at poll_interval= seconds and returns
the resource the moment it reaches a terminal state.
"""Single-point simulation: auth -> submit -> wait() -> read the canonical result.
Run it:
export THRUSTLAB_API_KEY=key_... # never hard-code the key
python examples/simulations/run_sync.py
The verification combo below (Spektrum Avian 4260 800 kV + 10.5x4.5 + Liperior
4S 5000 mAh @ 70% throttle, static) converges to ~9.78 N / ~8645 rpm. Swap in
your own component IDs, or resolve them by name with client.components.find(...).
"""
from thrustlab import Client
client = Client() # reads $THRUSTLAB_API_KEY from the environment
project = client.projects.create(name="sdk single-point example")
# Resolve components by name (find() returns the single match or raises
# AmbiguousComponentError / NotFoundError). Or paste explicit IDs instead:
# motor_id = "comp_motor_xxx"
motor = client.components.find(name="Spektrum Avian 4260 800Kv")
prop = client.components.find(name="10.5x4.5")
battery = client.components.find(name="Liperior 5000mAh 4S 35C")
sim = client.simulations.create(
project_id=project["id"],
battery_component_id=battery["id"],
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
rotor_groups=[
{
"label": "main",
"count": 4,
"motor_component_id": motor["id"],
"propeller_component_id": prop["id"],
"throttle_pct": 70,
}
],
)
print(f"created {sim['id']}, status={sim['status']}")
# wait() polls until completed / failed / canceled (or a timeout sentinel).
result = client.simulations.wait(sim["id"], timeout=300)
print(f"final status: {result['status']}")
if result["status"] == "completed":
# Canonical snake_case result (post-D-01): per-rotor group under its label
# index ("1", "2", ...); aggregate roll-ups under "All".
per_rotor = result["result"]["1"]
aggregate = result["result"]["All"]
print(f"per-rotor thrust: {per_rotor['thrust_n']:.2f} N")
print(f"per-rotor rpm: {per_rotor['rpm']:.0f}")
print(f"total thrust: {aggregate['total_thrust_n']:.2f} N")
Knobs (same defaults on all three resources):
| Argument | Default | Notes |
|---|---|---|
poll_interval | 2.0 s | Time between retrieve() calls |
timeout | 600.0 s | Wall-clock cap on the local poll loop |
If wait() exceeds timeout, the server-side run keeps going and the
SDK returns the resource with status: "timed_out" — a client-side
sentinel, never a server state. Resume polling later or subscribe via a
webhook in the meantime.
final = client.simulations.wait(sim_id, timeout=60)
if final["status"] == "timed_out":
# Resume later in this job, or in a follow-up job
final = client.simulations.retrieve(sim_id)
if final["status"] == "completed":
process(final["result"])
Pattern 2: subscribe via webhooks
For long-running sweeps or production pipelines where blocking is expensive, register a webhook endpoint and let the server push the terminal event.
endpoint = client.webhook_endpoints.create(
url="https://your-app.example.com/webhooks/thrustlab",
events=[
"simulation.completed", "simulation.failed",
"sweep.completed", "sweep.failed",
],
)
sim = client.simulations.create(project_id="proj_xxx", ...)
# `sim` is queued. Your handler will receive a signed event when terminal.
Receiving and verifying the event payload is a separate page — see webhook signature verification.
Pattern 3: poll yourself
If you want custom heartbeat/progress reporting, poll retrieve()
directly. status walks queued → running → {completed,failed,canceled}.
import time
from thrustlab import Client
client = Client()
sim = client.simulations.create(project_id="proj_xxx", ...)
while True:
cur = client.simulations.retrieve(sim["id"])
if cur["status"] in {"completed", "failed", "canceled"}:
break
print(f"… still {cur['status']}, sleeping 2 s")
time.sleep(2.0)
Terminal status values
| Status | Meaning |
|---|---|
completed | Result fields populated; ready to read |
failed | Inspect error.code and error.message |
canceled | User-canceled before completion (one 'l' on the wire) |
timed_out | SDK-side wait() sentinel — the server run is still going |
Cancelling
client.simulations.cancel(sim_id)
client.sweeps.cancel(sweep_id)
Cancellation is best-effort — a job already in its final ms of compute may
still finish as completed. Canceling an already-terminal resource is a
no-op (idempotent).
Polling vs webhooks: which to pick
| Situation | Pick |
|---|---|
| Interactive notebook / one-shot script | wait() polling |
| Single sim that finishes in < 60 s | wait() polling |
| Sweep with hundreds of points (5–15 min) | Either; webhooks save your job from blocking |
| Long batch pipeline / production cron | Webhooks |
| Public web handler (no long-blocking allowed) | Webhooks |
You can combine the two — kick off with create, return to your caller
immediately, and have a webhook handler complete the job downstream.
See also
- Simulations — the async simulation resource
- Sweeps — multi-point parameter studies
- Webhook endpoints — endpoint management
- Webhook signature verification — handler-side verification
- Async resources guide — HTTP-level contract