mode-sdk
Unofficial Python SDK for the Mode Analytics API — the documented REST surface plus the Discovery batch API. Not affiliated with Mode or ThoughtSpot.
- Python 3.11+, one runtime dependency (
httpx), fully typed - Lazy pagination, mapped errors, frozen dataclass models, retries with backoff
- Requires a Mode Business workspace — only resources inside one are reachable over the API
Install
uv add mode-sdk # or: pip install mode-sdk
Quickstart
from mode_sdk import Mode
# Explicit, or from MODE_WORKSPACE / MODE_API_TOKEN / MODE_API_SECRET
with Mode("acme", token=token, secret=secret) as mode:
mode.verify() # cheapest credential check
for space in mode.spaces.list(filter="all"): # default lists only *your* collections
for report in mode.reports.list(space=space):
print(report.token, report.name, report.created_at)
Credentials are keyword-only; each argument falls back to its environment variable. Every identifier argument takes the model a previous call returned, or its 12-character token string — passing the model is the safer spelling.
Examples
Run a report and read the results
run = mode.reports.run_and_wait(report, parameters={"country": "US"})
if run.succeeded:
for filename, csv in mode.report_runs.results_tables(report, run).items():
print(filename, len(csv))
else:
print(mode.report_runs.failure_detail(report, run))
The shape of an export is Mode's choice, not the report's: the same path answers text/csv for one report and application/zip for another. results_tables() returns {filename: csv} either way; results() returns a RunResults when you want the raw bytes and their media_type.
Save results to disk, or render a PDF
mode.report_runs.results(report, run).save("./exports") # a directory keeps Mode's filename
mode.exports.pdf(report, run).save("report.pdf") # starts and polls the async render
Create a report
from mode_sdk import query_spec
report = mode.reports.create(space, "revenue", [query_spec(sql, data_source_id)])
Mode documents no create-report endpoint, so this is three requests under the hood; a failure part-way deletes what was minted rather than leaving an unnamed report behind.
Handle errors
from mode_sdk import ModeError, NotFoundError, RateLimitError
try:
mode.reports.get(token)
except NotFoundError:
...
except RateLimitError as exc:
print(exc.retry_after) # the server's hint, in seconds
except ModeError: # the root: everything this package raises
raise
ModeAPIError subclasses map Mode's error bodies — BadRequestError, AuthenticationError, PermissionDeniedError, ConflictError, UnprocessableEntityError, InternalServerError. ModeConnectionError and ModeTimeoutError mean Mode never answered. httpx exceptions never escape.
Pagination
page = mode.reports.list(space=space)
for report in page: # lazily walks every page
...
page.first_page() # one request, one list
list(page) # everything, eagerly
One client, many threads
patient = mode.with_options(timeout=600.0, max_retries=0)
patient.report_runs.results(report, run) # same connection pool, longer deadline
Build one Mode and share it across threads: httpx.Client is thread-safe and models are frozen. Page objects are the exception — each belongs to the thread walking it.
Call an endpoint this package does not model
mode.request("GET", "/some/new/endpoint") # workspace-relative, returns the raw dict
Discovery API
from mode_sdk import Discovery, create_signature_token
signature = create_signature_token(
"acme", token=token, secret=secret, name="etl-reader", expires_at="2027-01-01T00:00Z"
)
# store signature.token / access_key / access_secret — the secret is returned exactly once
with Discovery("acme", signature=signature) as discovery:
for report in discovery.reports(include_spaces="all"):
print(report.token, report.name)
Read-only batch listings (reports, report_stats, queries, charts, collections, members) with a separate credential. Requires a Mode Enterprise plan; refusals raise DiscoveryUnavailableError.
What it covers
| Namespace | Endpoints |
|---|---|
mode.workspace |
verify, workspace, account |
mode.spaces |
Collections: get, list, create, update, delete, reports, datasets |
mode.space_memberships |
list, get, add, remove (deprecated by Mode) |
mode.reports |
get, list, create, update, delete, archive, unarchive, purge, run, run_and_wait |
mode.report_runs |
list, get, create, clone, duplicate, duplication_status, form_fields, results, results_tables, wait, create_and_wait, failure_detail |
mode.report_filters |
list, get, create, update, delete |
mode.queries |
list, get, create, update, delete |
mode.query_runs |
list, get, results |
mode.charts |
list, get |
mode.definitions |
list, get, create, update, delete |
mode.data_sources |
list, get, update, refresh_schema, purge |
mode.datasets |
get, list, update, delete, reports, fields, refresh_in_report |
mode.dataset_runs |
list, get, create |
mode.dataset_fields |
list, create, update, delete |
mode.memberships |
list, get, remove |
mode.invites |
create |
mode.groups |
list, get, create, update, delete, memberships, add_member, remove_member |
mode.audit_logs |
list |
mode.exports |
report_run, report_run_tables, query_run, pdf, from_href |
mode.report_schedules |
list, get, create, update, delete |
mode.report_subscriptions |
list, get, create, update, delete |
mode.dataset_schedules |
list |
Good to know
- Numeric-looking ids are strings (
report.id == "5747815"), and they do not route: Mode's paths take the 12-charactertoken, and the numeric id answers 404. Nothing is coerced. - Every model field is optional. Mode's key set varies by object type and endpoint; unmodelled keys stay in
model.raw, and an unparseable timestamp leaves the attributeNonewith the original string inraw. POSTis not retried by default — a replayed run-create starts a second run and bills the warehouse twice. Connection failures and 408/429/5xx on idempotent methods retry with full-jitter backoff; opt in per request withmode.transport.request(..., retry=True).- A
Retry-Afterabove 60 seconds stops the retry loop instead of sleeping minutes inside a library call; the hint comes back onRateLimitError.retry_after. per_pageis capped at 30 by Mode and ignored entirely by several collections; the page walk absorbs both and still returns the whole collection.- Logging uses the standard library:
logging.getLogger("mode_sdk").setLevel(logging.DEBUG). Header values, bodies, query strings, tokens and secrets are never logged at any level. - Bring your own HTTP client with
Mode(..., http_client=httpx.Client(...)). That hands you HTTP policy, so combining it withtimeout,proxy,verifyand friends raises rather than being silently ignored.
Development
uv sync # create the venv from uv.lock
uv run pytest # offline suite — no network, no credentials
uv run ruff format && uv run ruff check --fix
uv run pyright # strict
Fixtures are hand-written payloads and every request is mocked; tests must stay offline, with synthetic tokens and names only.
Licence
Apache-2.0
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 mode_sdk-0.1.0.tar.gz.
File metadata
- Download URL: mode_sdk-0.1.0.tar.gz
- Upload date:
- Size: 49.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fcf41c78d3e84d27fffabafe85b067fa2991eae75f537e6deba5b9aa12df159
|
|
| MD5 |
5c386b7bf27cb3b21afcde5d8b12fa10
|
|
| BLAKE2b-256 |
860806d10da2a0e7feec88e652ff363458801cd2fb49244d0ed8f051ae1c9020
|
Provenance
The following attestation bundles were made for mode_sdk-0.1.0.tar.gz:
Publisher:
release.yml on moonD4rk/mode-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mode_sdk-0.1.0.tar.gz -
Subject digest:
1fcf41c78d3e84d27fffabafe85b067fa2991eae75f537e6deba5b9aa12df159 - Sigstore transparency entry: 2395938658
- Sigstore integration time:
-
Permalink:
moonD4rk/mode-sdk@5df1bdb841d3fde706ac50e11ebdd75685187966 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/moonD4rk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5df1bdb841d3fde706ac50e11ebdd75685187966 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file mode_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mode_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 62.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2986cc2c643a59d7087589e8a1f5a1f356bc3c6ae14cf7b0907b7f6979733d7
|
|
| MD5 |
12dd77d45ed20f122d4847a43f203129
|
|
| BLAKE2b-256 |
976c31c86b64995c0e691af82928074aa633a7d85d69b7c6f6898836368f3b96
|
Provenance
The following attestation bundles were made for mode_sdk-0.1.0-py3-none-any.whl:
Publisher:
release.yml on moonD4rk/mode-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mode_sdk-0.1.0-py3-none-any.whl -
Subject digest:
d2986cc2c643a59d7087589e8a1f5a1f356bc3c6ae14cf7b0907b7f6979733d7 - Sigstore transparency entry: 2395938944
- Sigstore integration time:
-
Permalink:
moonD4rk/mode-sdk@5df1bdb841d3fde706ac50e11ebdd75685187966 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/moonD4rk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5df1bdb841d3fde706ac50e11ebdd75685187966 -
Trigger Event:
workflow_dispatch
-
Statement type: