Python client for the Takeoff hydrofoil design platform
Project description
pytakeoff
Python client for the Takeoff hydrofoil design platform.
Everything the web app can do — projects, foil sections, analysis, optimization — scripted from Python. pytakeoff speaks the same WebSocket command protocol as the frontend, so every server command is available, and all permissions, credits, and rate limits are enforced server-side exactly as in the browser.
Install
pip install pytakeoff
Authentication — API keys, never passwords
Scripts authenticate with an API key — a token shaped like
tk_<id>_<secret>, never your password. Get one either way:
-
In the GUI: Account → API Keys → Generate API key (shown once — copy it).
-
From the terminal (one-time interactive setup):
python -m pytakeoff # create a NEW key (asks for your password) and save it python -m pytakeoff configure # save a key you ALREADY made in the GUI (paste it)
The first form asks for your username/password once, creates a key over a single request, and saves it to
~/.takeoff/credentials. The password is never stored.
Once saved, no key in your code — TakeoffClient() picks it up:
from pytakeoff import TakeoffClient
with TakeoffClient() as client: # key read from your saved credentials
print(client.username)
TakeoffClient resolves the key, in order, from the api_key= argument, the
TAKEOFF_API_KEY environment variable (recommended for CI), or your saved
~/.takeoff/credentials file. Never commit a key to a repository. Leaked a
key? Revoke it in the GUI; your password and other keys are unaffected.
Quickstart
from pytakeoff import TakeoffClient
# Your API key (GUI → Account → API Keys). Or omit api_key= entirely and it is
# resolved from TAKEOFF_API_KEY / ~/.takeoff/credentials. Never commit a real key.
API_KEY = "tk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Connects to the official https://app.takeoff-technologies.com by default —
# no port needed. Self-hosted / dev: TakeoffClient("http://localhost", api_key=...)
# (plain-http URLs get port 8000 applied internally).
with TakeoffClient(api_key=API_KEY) as client:
# Projects
for p in client.projects.list():
print(p["name"])
project = client.projects.open("my_foil")
# Entities (FoilSection, Wing, Bulb, Hull, Sail, ...)
sections = project.foil_sections()
project.update_entity("FoilSection", {"id": "<section-id>", "name": "renamed"})
# 2D polar analysis (returns Plotly figure JSON among other data)
polars = project.compute_polars(alpha_range=[-10, 10], reynolds_million=1.0)
# Optimization with live progress
result = project.optimize(
"<optiaerofoil-entity-id>",
on_progress=lambda pct, msg: print(f"{pct}% {msg or ''}"),
timeout=None, # wait as long as it takes
)
project.save()
Examples — simplest to more complex
Runnable scripts in examples/, numbered in learning order:
01_create_api_key.py— one-time setup: create an API key from the terminal (or use the GUI) and copy it into your scripts.02_connect.py— connect and print who you are.03_projects.py— list your projects and show the currently open one (client.projects.current()).04_foil_section.py— the high-levelFoilSectionAPI: get/set control points, geometric parameters, structural parameters.05_polar_analysis.py— 2D analysis: configure a sweep, run it, work with the raw numbers (incl. chordwise Cp).06_optimization.py— 2D optimization: configure objectives/constraints, run with progress, inspect the result.
Prefer notebooks? The same three topics with plots: 07_foil_section.ipynb, 08_analysis_2d.ipynb, 09_optimization_2d.ipynb.
Foil sections
project.foil_sections() / project.foil_section(name) return high-level handles with three get/set pairs. Getters fetch live values; setters apply the change and return the achieved values (parametric setters refit the B-spline, so they can differ marginally from the request). Units match the GUI: tc, camber, te_thickness are percent of chord.
section = project.foil_section("main_foil")
cp = section.control_points() # {"upper": [[x,y],...], "lower": [...], "degree", "n_coefs"}
section.set_control_points(upper=cp["upper"], lower=cp["lower"])
geo = section.geometry() # {"tc", "camber", "le_radius", "te_thickness", "te_angle"}
section.set_geometry(tc=12.0, camber=2.5)
struct = section.structure() # {"area", "Ixx", "SMx", "J", "centroid"(read-only)}
section.set_structure(area=struct["area"] * 1.1)
xy = section.points() # the computed outline coordinates
2D analysis
project.analysis_2d(...) holds sweep parameters (alpha_range, reynolds_million, flap_range/flap_lock, ncrit, mach, solver, fluid, ...) and runs them over the visible foil sections:
analysis = project.analysis_2d(alpha_range=[-10, 10], reynolds_million=1.0, flap_lock=True)
result = analysis.run() # raw arrays: alpha, Cl, Cd, Cm, Cl_Cd, ..., top_x/top_Cp (chordwise)
figures = analysis.figures() # or the web app's ready-made Plotly figures
2D optimization
project.create_optimization_2d(...) / project.optimization_2d(name) return an Optimization2D handle on an OptiAeroFoil entity:
opt = project.create_optimization_2d("my_opt", initial_section="main_foil", solver="NN",
optimizer_config={"maxiter": 100, "tol": 1e-3})
opt.set_objectives([{"func": "maxGlide", "Re": 1e6, "alpha": 4.0, "solve_for": "fixed"}])
opt.set_constraints(geo=[{"variable": "get_tc", "operator": ">", "value": 0.10}])
response = opt.run(on_progress=lambda pct, msg: print(pct, msg), timeout=None)
r = opt.result() # latest run: opt_data (scores), opt_section (geometry)
opt.save_result() # optimized section -> project as a FoilSection
opt.restore("1") # reload a stored run; opt.runs(), opt.delete_runs([...])
Objective func is "minCd", "maxCl", "maxGlide", or any min<var>/max<var> over the polar variables; solve_for is "fixed", "alpha" (hit target_Cl), or "flap".
Every command, not just the facade
The facade (client.projects, Project) covers the common workflows. Everything else goes through the generic escape hatch — any of the server's registered commands can be called directly:
client.call("wing_set_span", wing={"id": wing_id}, span=1.25)
client.call("run_simulation", entity_id=sim_id, on_progress=print, timeout=None)
Discover what the server offers (fetched live, always in sync with the server version):
commands = client.commands() # {name: {description, credit cost, ...}}
print(sorted(commands))
Server-initiated events
The server pushes broadcasts (saves by collaborators, property updates, job progress). Subscribe with:
client.on("project_saved", lambda msg: print("saved:", msg))
client.on("*", lambda msg: ...) # everything
Handlers run while a call() is waiting for its response — keep them fast and don't issue new commands from inside one.
Error handling
import time
from pytakeoff import (
CommandError, CommandTimeout, ConnectionClosed, GuiSessionActive, RateLimited,
)
try:
client.call("run_optimization", entity_id=opt_id, timeout=3600)
except RateLimited as e:
time.sleep(e.retry_after) # server throttled us; wait and retry
except GuiSessionActive:
print("Close your browser session to switch projects from a script")
except CommandError as e:
print("Server rejected:", e, e.payload) # payload carries structured details
except CommandTimeout:
print("Took too long")
except ConnectionClosed:
client.reconnect() # re-exchanges the API key if needed
Good to know
- Scripts are rate limited. API-key connections have per-account command budgets (a per-minute cap, plus a tighter hourly cap on heavy commands like
run_simulation/run_optimization), one heavy command in flight at a time, and a small cap on concurrent connections. Limits scale with your plan. The browser GUI is not affected. Hitting a limit raisesRateLimitedwithretry_after. - One session per user. The server keeps a single session per account, shared between your scripts and your browser. While your GUI is open, entity edits from a script are allowed (the GUI shows a "Script connected" chip and can refresh), but project switching/closing commands raise
GuiSessionActive. - Credits & permissions apply exactly as in the web app; paid commands bill your account.
- Binary mesh data: a few commands (
get_entity_mesh,get_simulation_visualization, ...) answer with raw FlatBuffer bytes.pytakeoffreturns them as aFlatBufferResultwithout decoding — most scripting workflows never need them. - Long sessions: session tokens expire after ~1 hour; if the connection drops,
client.reconnect()re-exchanges your API key and reconnects.
Development
git clone https://github.com/takeoff-technologies/pytakeoff.git
cd pytakeoff
pip install -e ".[dev]"
License
Apache-2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytakeoff-0.1.1.tar.gz.
File metadata
- Download URL: pytakeoff-0.1.1.tar.gz
- Upload date:
- Size: 238.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
217c8fda573a21688af987cd71372886ef3a878aa133b34eec38ba83c41406dc
|
|
| MD5 |
6c83f99c12b85bbf89ee64a8f4069c4f
|
|
| BLAKE2b-256 |
3e27e87a3d5d744609c1c1e4fed0d47e7128ef2281f3c8e28e0f57f3349a6ebf
|
Provenance
The following attestation bundles were made for pytakeoff-0.1.1.tar.gz:
Publisher:
publish.yml on takeoff-technologies/pytakeoff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytakeoff-0.1.1.tar.gz -
Subject digest:
217c8fda573a21688af987cd71372886ef3a878aa133b34eec38ba83c41406dc - Sigstore transparency entry: 2187923475
- Sigstore integration time:
-
Permalink:
takeoff-technologies/pytakeoff@13741f3bb80da6aae212e8903e172ae2edc817cf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/takeoff-technologies
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@13741f3bb80da6aae212e8903e172ae2edc817cf -
Trigger Event:
release
-
Statement type:
File details
Details for the file pytakeoff-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pytakeoff-0.1.1-py3-none-any.whl
- Upload date:
- Size: 39.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b41f4ac3d6a10b6f293d7b565feab2e858c6c81be1af7c6b381d0c28831a4b7
|
|
| MD5 |
96fb644f5ac167afd3cda573fccdc921
|
|
| BLAKE2b-256 |
eff98f9314a4e57a79964f78bc052129c4cd4ce789903adde2add372a4467f9b
|
Provenance
The following attestation bundles were made for pytakeoff-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on takeoff-technologies/pytakeoff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytakeoff-0.1.1-py3-none-any.whl -
Subject digest:
2b41f4ac3d6a10b6f293d7b565feab2e858c6c81be1af7c6b381d0c28831a4b7 - Sigstore transparency entry: 2187923490
- Sigstore integration time:
-
Permalink:
takeoff-technologies/pytakeoff@13741f3bb80da6aae212e8903e172ae2edc817cf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/takeoff-technologies
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@13741f3bb80da6aae212e8903e172ae2edc817cf -
Trigger Event:
release
-
Statement type: