Components catalog
Accessor: client.components
The catalog of motors, propellers, and batteries. You reference
component ids in simulation and sweep bodies; create() adds your own
private custom components. Custom components you submit through
submissions appear here once approved.
list(), retrieve() and brands() are readable without a credential and
return the curated public catalog. A credential adds your own components and
starred items, and is required for create() and every other method.
Methods
| Method | Endpoint | Returns |
|---|---|---|
client.components.list(**filters) | GET /v1/components | CursorPager of components |
client.components.find(**filters) | GET /v1/components | Single component (one-hit-or-raise) |
client.components.create(type=..., name=..., spec_json=...) | POST /v1/components | New private component |
client.components.retrieve(component_id) | GET /v1/components/{id} | Single component |
client.components.brands() | GET /v1/components/brands | List of {name, count} brand summaries |
Filter by type
Every simulation needs at least one motor, one propeller, and one battery.
The type filter is the fastest way to scope the catalog:
from thrustlab import Client
client = Client()
motors = list(client.components.list(type="motor", limit=100))
print(f"{len(motors)} motors in catalog")
Valid type values: motor, propeller, battery. Anything else —
esc included, ThrustLab doesn't catalog ESCs — is rejected with a 422.
Filter by brand
brand is a discrete filter that takes the bracket [in] suffix, with a
comma-separated list of names:
kde_or_tmotor = client.components.list(
type="motor",
**{"brand[in]": "KDE,T-Motor"},
)
for m in kde_or_tmotor:
print(m["id"], m["name"], m["spec_json"].get("brand"))
The full list of brands, with per-brand counts, is available via
client.components.brands():
brands = client.components.brands()
for b in brands["data"]:
print(b["name"], b["count"]) # KDE 42, T-Motor 38, ...
Filter motors by Kv rating
Numeric spec fields take a bracket-suffixed filter — kv[gte] / kv[lte]
(also [gt] / [lt], currently treated the same as the inclusive forms).
Pass them through **extra_filters:
mid_kv = client.components.list(type="motor", **{"kv[gte]": 400, "kv[lte]": 900})
for m in mid_kv:
print(m["id"], m["name"], m["spec_json"]["kv"])
Type-specific filters
Every numeric spec filter follows the key[gte] / key[lte] pattern.
[gt] and [lt] are also accepted and currently behave like the inclusive
forms. Each filter maps to a spec_json field server-side.
Both spellings work. The filter key and the sort key used to be different names
for the same field: resistance[lte] filtered but sort_by=resistance
returned 400, while sort_by=R sorted and R[lte] did nothing. Both the wire
alias and the spec_json key are now accepted on both, so resistance[lte] is
R[lte] and sort_by=resistance is sort_by=R. The same holds for every row
below.
| Type | Filter key | spec_json field |
|---|---|---|
motor | kv[gte] / kv[lte] | kv |
motor | weight[gte] / weight[lte] | weight |
motor | resistance[gte] / resistance[lte] | R |
motor | current_max[gte] / current_max[lte] | iq_max |
motor | poles[gte] / poles[lte] | n |
motor | stator_diameter[gte] / stator_diameter[lte] | diameter |
motor | stator_length[gte] / stator_length[lte] | length |
motor | voltage_nominal[gte] / voltage_nominal[lte] | u_nominal |
propeller | diameter[gte] / diameter[lte] | diameter |
propeller | pitch[gte] / pitch[lte] | pitch |
propeller | weight[gte] / weight[lte] | weight |
propeller | max_rpm[gte] / max_rpm[lte] | maxRPM |
propeller | inertia[gte] / inertia[lte] | inertia |
battery | series_cells[gte] / series_cells[lte] | series_cells |
battery | capacity[gte] / capacity[lte] | total_capacity_mAh |
battery | c_rating[gte] / c_rating[lte] | c_rating |
battery | weight[gte] / weight[lte] | weight_g |
A motor's stator_diameter and stator_length are the STATOR dimensions — the
22 and the 7 in "2207" — not the can. The voltage_nominal filter reads
u_nominal, the datasheet voltage used to measure the motor's no-load current.
Non-numeric fields take a comma-separated [in] list instead:
| Type | Filter key | spec_json field |
|---|---|---|
| any | brand[in] | brand |
battery | chemistry[in] | chemistry |
propeller | blades[in] | num_blades |
propeller | rotation[in] | rotation |
propeller | maker[in] | type |
A propeller's maker is spelled maker on the wire because type already means
the component type in every envelope.
A bracket filter key with a valid operator but an unknown field name for that
type returns 400 unknown_filter, and the message lists the valid keys for
that type. It used to be dropped silently, which turned a typo into a
full-catalog result with no warning.
The list response also echoes the bounds it applied as filters, keyed by wire
name, so you can confirm what actually bound rather than inferring it from the
row count.
# 14-16 inch propellers with at least 8 inch pitch
props = client.components.list(
type="propeller",
**{"diameter[gte]": 14, "diameter[lte]": 16, "pitch[gte]": 8},
)
Motor resistance in list rows
Motor phase resistance R, in ohms, appears in list rows and on
GET /v1/components/{id}. The value is the same on every tier. You need no account
to read it on GET /v1/public/motors.
R is the spec_json key and resistance is the wire alias. Both work as a
filter and as a sort key.
low_r = client.components.list(
type="motor",
sort_by="resistance",
**{"kv[lte]": 900, "resistance[lte]": 0.05},
)
for m in low_r:
print(m["name"], m["spec_json"]["R"], "ohm")
Other list filters
| Filter | Type | Notes |
|---|---|---|
visibility | "public" | "private" | Restrict to the shared catalog or your own private customs. Omit for both. |
search (or q) | str | Case-insensitive substring match on name. If both are supplied, search wins. |
sort_by | str | name, created_at, or (with type set) a type-specific spec key. Either spelling: resistance or R. Two keys are computed rather than stored: pd_ratio on a propeller is pitch divided by diameter, and energy_density on a battery is its nominal specific energy in Wh/kg (capacity x 3.7 V per cell x series cells / mass) — a label figure for ranking packs, not a measurement. |
sort_order | "asc" | "desc" | Default asc. |
ids_only | bool | Every matching id in one response, capped at 5000. truncated is true when the cap bound the result. |
The ids_only cap is 5000. It was 2000, which is one row short of useful on a
2005-row catalog.
A cursor is bound to the sort_by and sort_order it was issued for. Changing
either mid-scan invalidates the cursor; start a new scan instead.
See the API reference for the full list of per-type sort keys and spec filter keys.
The same grammar with no account
The open routes — GET /v1/public/motors, /v1/public/propellers,
/v1/public/batteries — take the same search (and q), repeatable brand,
sort_by, sort_order, bracket ranges and [in] checklists, over the curated
public catalog and with no credential. One difference: a motor cannot be sorted
by power_max there, because maximum power depends on a pack voltage a reader
browsing the catalog has not chosen; current_max is the motor limit that does
not. Paging is starting_after rather than cursor.
The open motor list and detail routes include curve_pack_cells, the cell count
of the pack used for published curves, or null when none are published. Use
curve_pack_cells[gte] / curve_pack_cells[lte] on GET /v1/public/motors for
inclusive limits in cells. GET /v1/public/motors/facets reports the smallest
and largest values in ranges.curve_pack_cells.
Two reads exist only on the open routes:
GET /v1/public/{type}/facets— every filterable value with its row count, and the smallest and largest value of each numeric field, over the same rows the list returns. What a filter rail is built from.GET /v1/public/{type}/by-slug/{slug}— one component by the slug its catalog page uses, or{"object": "redirect", "slug": "..."}when the slug has moved.
Retrieve one component
motor = client.components.retrieve("comp_2c5tQ...")
print(motor["spec_json"])
A component you created and own returns its full raw spec_json. Catalog rows
return the same datasheet fields on every tier: brand, name,
type and label, plus
- Motor:
kv,weight,power_max,iq_max,diameter,length,u_nominal,topology,R,n,iq_nominal,io_nominal,reduction_ratio,L,inertia. - Propeller:
diameter,pitch,num_blades,weight,maxRPM,rotation,inertia. - Battery:
chemistry,series_cells,parallel_cells,total_capacity_mAh,c_rating,weight_g,length_mm,width_mm,height_mm,form_factor,cell_model,pack_resistance_mOhm,internal_resistance_mOhm.
GET /v1/components/{id}/specs returns that same projection. It is not
metered, on any type or tier.
Catalog propeller rows do not include blade geometry. The per-station radius,
chord, twist, thickness and section_airfoils tables are returned by no
endpoint, on any tier.
Create your own
create() persists a private custom component and returns its id:
prop = client.components.create(
type="propeller",
name="My Custom 10.5x4.5",
spec_json={...},
)
The per-type required spec_json fields, units, and validation rules are in
Creating your own components.
Recipe: pick the highest-thrust prop for a given motor
Narrow the propellers with the list filters, then sweep them as one component axis at a fixed throttle. A loop that submits one simulation per propeller spends one submission and one cold solve per candidate; this is one request.
motor_id = "comp_2c5tQ..."
props = client.components.list(
type="propeller",
**{"diameter[gte]": 10, "diameter[lte]": 14},
limit=20,
).data
sweep = client.sweeps.create(
project_id="proj_xxx",
battery_component_id="comp_xxx",
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": props[0]["id"],
"throttle_pct": 100,
}],
sweep={
"rotor_sweep_mask": [True],
"component_axes": [
{"axis": "propeller", "slots": [0],
"component_ids": [p["id"] for p in props]},
],
},
)
client.sweeps.wait(sweep["id"], timeout=900)
ranked = sorted(
(
(pt["inputs"]["component"][0]["name"], pt["rotors"]["1"]["thrust_n"])
for pt in client.sweeps.list_points(sweep["id"])
),
key=lambda r: -r[1],
)
for name, thrust in ranked[:5]:
print(f"{name}: {thrust:.1f} N")
See Sweeps for the component-axis shape and the per-minute submission limits it keeps you clear of.
See also
- Submissions — submit a custom component for review
- Starred components — per-project favorites