Pagination
Every list endpoint returns a CursorPager. You can either iterate the
pager (which lazily fetches subsequent pages), or read .data and
.has_more directly for "show more"-style UI.
Auto-iterate every page
The simplest mode — the pager fetches the next page transparently when you exhaust the current one.
from thrustlab import Client
client = Client()
for project in client.projects.list():
print(project["id"], project["name"])
This is safe even on huge result sets: the pager only fetches one page at a time. Break out early to stop:
recent = []
for sim in client.simulations.list():
recent.append(sim)
if len(recent) >= 100:
break
Manual cursor (UI flow)
For "load more" buttons or infinite scroll, fetch one page and check
has_more:
page = client.projects.list(limit=20)
items = page.data # list[dict] — first 20
print(f"has_more: {page.has_more}")
if page.has_more:
last_id = items[-1]["id"]
page2 = client.projects.list(limit=20, cursor=last_id)
items.extend(page2.data)
cursor= is the SDK-wide pagination kwarg on every list() method — the
opaque next_cursor token from the previous response (falls back to the
last item's id if the response omits next_cursor). The server returns the
next page after that cursor.
All list parameters
| Parameter | Default | Range | Notes |
|---|---|---|---|
limit | 20 | 1..100 (up to 1..1000 on simulations, sweeps, and dynamic_simulations lists) | Page size — items returned per server hit |
cursor | None | opaque token | Pagination cursor — next_cursor from the previous page |
Resource-specific filters are layered on top — see each resource's own
methods table (e.g. components.list(type=...),
simulations.list(project_id=...)).
What CursorPager exposes
page = client.projects.list(limit=20)
page.data # list[dict] — items on the current page
page.has_more # bool — True if there are more pages
for item in page: # iter walks every page until exhausted
...
You can iterate the pager once. To re-iterate, call list(...) again.
One-hit-or-raise: .one() and .first()
For "I expect exactly one match" lookups, .one() and .first() skip the
iteration dance. Both are bounded — they only inspect the first page,
they never exhaust the pager:
# Raises if zero or more than one match.
motor = client.components.list(type="motor", search="T-Motor F80").one()
# Returns the first item, or None if there are none — never raises.
motor = client.components.list(type="motor", search="T-Motor F80").first()
.one() raises NotFoundError for zero matches, and AmbiguousComponentError
(from thrustlab.exceptions, a direct ThrustlabError subclass — not an
APIError) if more than one item matched, including the case where exactly
one item is on the first page but the pager still has more pages. The
exception carries candidates (aliased matches) so you can disambiguate:
from thrustlab.exceptions import AmbiguousComponentError, NotFoundError
try:
motor = client.components.list(type="motor", search="F80").one()
except AmbiguousComponentError as exc:
motor = next(c for c in exc.candidates if c["name"] == "T-Motor F80 Pro")
except NotFoundError:
motor = None
client.components.find(...) is a convenience wrapper over
.list(**filters).one() — see Components.
Order
Server-default ordering is by created-at descending (newest first), and within a single created-at the tiebreaker is the resource id. Cursors preserve this order across pages.
Recipe: collect every result into one list
all_sims = list(client.simulations.list(project_id="proj_xxx"))
print(f"total: {len(all_sims)}")
For a million-row table this loads everything into memory — for huge
collections, prefer streaming with for ... in.
Recipe: server-side filter then paginate
list() accepts resource-specific filters as keyword args. Apply them at
the server, not in your loop, to avoid round-trips on rows you don't care
about.
# All KDE motors with Kv between 400 and 900, lazily paginated
for motor in client.components.list(
type="motor", brand_in=["KDE"], kv_gte=400, kv_lte=900
):
print(motor["id"], motor["spec_json"]["kv_rpm_per_v"])
Recipe: parallel pagination across many resources
Fetch pages from independent resources concurrently using concurrent.futures:
from concurrent.futures import ThreadPoolExecutor
from thrustlab import Client
client = Client()
def first_page(resource_name):
return list(getattr(client, resource_name).list(limit=20).data)
with ThreadPoolExecutor(max_workers=4) as ex:
futures = {
name: ex.submit(first_page, name)
for name in ("projects", "simulations", "sweeps", "webhook_endpoints")
}
for name, fut in futures.items():
print(name, len(fut.result()))
The SDK's underlying httpx.Client is thread-safe, so concurrent calls
through one Client instance are fine.
See also
- Pagination guide — HTTP-level cursor contract
- Components — example of layered filters
- Sweeps —
list_pointsis the same shape