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.

Methods

Methods
MethodEndpointReturns
client.components.list(**filters)GET /v1/componentsCursorPager of components
client.components.find(**filters)GET /v1/componentsSingle component (one-hit-or-raise)
client.components.create(type=..., name=..., spec_json=...)POST /v1/componentsNew private component
client.components.retrieve(component_id)GET /v1/components/{id}Single component
client.components.brands()GET /v1/components/brandsList 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-specific filters
TypeFilter keyspec_json field
motorkv[gte] / kv[lte]kv
motorweight[gte] / weight[lte]weight
motorresistance[gte] / resistance[lte]R
motorcurrent_max[gte] / current_max[lte]iq_max
motorpoles[gte] / poles[lte]n
propellerdiameter[gte] / diameter[lte]diameter
propellerpitch[gte] / pitch[lte]pitch
propellerweight[gte] / weight[lte]weight
propellermax_rpm[gte] / max_rpm[lte]maxRPM
propellerinertia[gte] / inertia[lte]inertia
batteryseries_cells[gte] / series_cells[lte]series_cells
batterycapacity[gte] / capacity[lte]total_capacity_mAh
batteryc_rating[gte] / c_rating[lte]c_rating
batteryweight[gte] / weight[lte]weight_g

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, is in the list-row projection and on GET /v1/components/{id} for every tier that sees specs. The server would already sort and filter on it, but the value itself was readable only through the metered datasheet endpoint, so ranking motors by resistance meant reconstructing each one's bracket from a series of count queries.

On the free tier, catalog motor and battery rows return only brand, name, type and label, with no spec numbers. Catalog propellers return the same headline fields on every tier. Free-tier callers receive R for custom motors they own, but not for catalog 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")

GET /v1/components/{id}/specs on a catalog motor or battery is metered on a budget of distinct components, so filter and rank with the list endpoint first. See Rate limits.

Other list filters

Other list filters
FilterTypeNotes
visibility"public" | "private"Restrict to the shared catalog or your own private customs. Omit for both.
search (or q)strCase-insensitive substring match on name. If both are supplied, search wins.
sort_bystrname, created_at, or (with type set) a type-specific spec key. Either spelling: resistance or R.
sort_order"asc" | "desc"Default asc.
ids_onlyboolEvery 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.

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 a fixed headline allowlist, subject to tier access:

  • Motor: kv, weight, power_max, iq_max, diameter, length, u_nominal, topology, R.
  • Propeller: diameter, pitch, num_blades, weight, maxRPM, rotation.
  • Battery: chemistry, series_cells, parallel_cells, total_capacity_mAh, c_rating, weight_g, length_mm, width_mm, height_mm, form_factor, cell_model.

Catalog propeller rows do not include blade geometry such as chord and twist tables.

GET /v1/components/{id}/specs returns a wider datasheet view for catalog motors, adding n, iq_nominal, io_nominal and reduction_ratio. The read is metered for catalog motors and batteries and carries the budget headers; your own custom components and propellers are not metered. See Rate limits.

The API never returns L or inertia for catalog motors, on any tier or through any endpoint.

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