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

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
motorstator_diameter[gte] / stator_diameter[lte]diameter
motorstator_length[gte] / stator_length[lte]length
motorvoltage_nominal[gte] / voltage_nominal[lte]u_nominal
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 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-specific filters (table 2)
TypeFilter keyspec_json field
anybrand[in]brand
batterychemistry[in]chemistry
propellerblades[in]num_blades
propellerrotation[in]rotation
propellermaker[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

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. 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_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.

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