Skip to main content

Python SDK reference

This page lists the public API of taruvi 0.2.3: each module, its methods, and the HTTP request each method sends. The sync and async clients have the same modules and methods. In the async client, every method that sends a request is a coroutine; builder methods such as filter() are not.

Request paths are relative to your site address, and APP_SLUG is the client's app_slug. Most module methods also accept app_slug= to target another app on the same site. For setup, start with Python SDK.

Client#

Client(api_url, app_slug, ...) holds your site address, your app, and the credential. Its modules, such as client.database, send the requests.

import os

from taruvi import Client

client = Client(
os.environ["TARUVI_SITE_URL"],
os.environ["TARUVI_APP_SLUG"],
mode="sync",
timeout=120,
max_retries=3,
api_key=os.environ["TARUVI_API_KEY"],
)

Arguments

  • api_url (required) — Your site address, TARUVI_SITE_URL. A trailing slash is removed.
  • app_slug (required) — The app's slug, TARUVI_APP_SLUG.
  • mode — "sync" or "async". Detected from the running event loop when omitted.
  • timeout — Seconds to wait for each request: a whole number from 1 to 300.
  • max_retries — Extra attempts after a timeout or connection failure. See Timeouts and retries.
  • api_key, jwt, session_token — The credential. Passing one replaces any in the environment; see Authenticate with the Python SDK.

Members

  • database, storage, functions, secrets, policy, users, app, settings, analytics, auth — The modules below.
  • is_authenticated — Whether the client holds a credential.
  • config — The resolved TaruviConfig.
  • close() — Closes the HTTP connection pool. It also runs when a with or async with block exits.

Every request identifies the SDK with X-Taruvi-Client and User-Agent headers, such as taruvi-python/0.2.3 (python/3.12.4).

Auth#

client.auth signs in and reads the current user. The sign-in methods return a new client and leave the original unchanged.

Sign in with a password#

POST /_allauth/app/v1/auth/login
import os

user_client = client.auth.signInWithPassword(
os.environ["TARUVI_USER_EMAIL"],
os.environ["TARUVI_USER_PASSWORD"],
)

Returns a new client that sends the returned JWT. Raises AuthenticationError when sign-in fails.

Sign in with a token#

import os

session_client = client.auth.signInWithToken(
os.environ["TARUVI_SESSION_TOKEN"],
token_type="session_token",
)

token_type is "jwt" (the default), "api_key", or "session_token". Returns a new client with that credential. It sends no request and isn't a coroutine.

Sign out#

signOut() returns a copy of the client with no credential. It sends no request and isn't a coroutine.

Get the current user#

GET /api/users/me/
me = client.auth.get_current_user()
print(me["data"]["username"])

Returns the response envelope, with the user under ["data"].

Database#

client.database.from_(table) returns a query builder. The builder is mutable: each method updates it and returns it. execute(), first(), and count() send the request. Request paths below are relative to /api/apps/APP_SLUG/datatables/TABLE/.

Call an operation such as get(), create(), or delete() last, right before execute(). Invalid use, such as update() without a record ID, raises ValueError before anything is sent.

List records#

Reads the records that match the query. Always set a page size: without page_size(), a list read returns every matching row and page() has no effect.

GET …/data/
(
client.database.from_("tasks")
.sort("title", "asc")
.page(1)
.page_size(20)
.execute()
)
  • sort(field, order="asc") — Adds a sort key; call it again to add a tie-breaker. Sent as ordering.
  • order_by(*fields) — Several keys at once: order_by(("title", "desc"), "id") or order_by("-title", "id").
  • page_size(n) — Records per page, up to 1,000 by default; a larger value raises ValidationError.
  • page(n) — The page number, starting at 1.

Returns {"data": [...], "total": N}, where total counts every match.

Filter records#

filter(field, operator, value) adds a condition. eq sends the bare field name, and other operators send field__operator. List values for in, nin, between, and similar operators are joined with commas. Repeating the same field and operator keeps only the last value.

GET …/data/?done=false&title__icontains=docs
(
client.database.from_("tasks")
.filter("done", "eq", False)
.filter("title", "icontains", "docs")
.execute()
)

For or conditions and nesting, pass a filter tree, as a dict or list. It's sent as JSON in filters, and TaruviBase combines it with any flat filters using AND:

GET …/data/?filters=…
(
client.database.from_("tasks")
.filter([
{
"operator": "or",
"value": [
{"field": "done", "operator": "eq", "value": False},
{"field": "title", "operator": "icontains", "value": "urgent"},
],
}
])
.execute()
)

The operators are the same as in the JavaScript SDK; see Choose a filter operator.

Expand related records#

GET …/data/?populate=assignee,project
client.database.from_("tasks").populate("assignee", "project").execute()

populate(*fields) expands the named relationships, and populate_all() expands every first-level relationship with populate=*. See Relationships.

GET …/data/?search=invoice
client.database.from_("tasks").search("invoice").execute()

Full-text search, on tables that have a search vector.

GET …/data/?embedding__vector_near=[…]&_topk=5
query_vector = [0.12, -0.03, 0.88]
(
client.database.from_("documents")
.vector_search("embedding", query_vector, topk=5)
.execute()
)
  • vector_search(field, query_vector, *, topk=10, threshold=None, ef_search=None, metric=None) — Nearest-neighbour search on a vector field. threshold drops results beyond that distance, and metric is "cosine", "l2", or "ip".
  • hybrid(*, strategy="rrf", alpha=0.5) — Combines vector_search() with search(). alpha runs from 0.0, text only, to 1.0, vectors only.

See Search.

Aggregate#

GET …/data/?_aggregate=count(*)&_group_by=done&_having=count__gte=10
(
client.database.from_("tasks")
.aggregate("count(*)")
.group_by("done")
.having("count__gte=10")
.execute()
)
  • aggregate(*expressions) — Aggregates such as "count(*)" or "sum(total)".
  • group_by(*fields) — Groups the results.
  • having(condition) — Filters the groups by an aggregate alias, such as "count__gte=10".

See Aggregations.

Include allowed actions#

GET …/data/?allowed_actions=update,delete
client.database.from_("tasks").allowed_actions(["update", "delete"]).execute()

Each row gains _allowed_actions: the ones among update and delete that the caller may perform on it.

Get a record#

GET …/data/ID/
client.database.from_("tasks").get("TASK_ID").execute()

Returns the response envelope, with the record in data. A missing record raises NotFoundError.

Get the first record or a count#

client.database.from_("tasks").filter("done", "eq", False).first()
client.database.from_("tasks").filter("done", "eq", False).count()
  • first() — The first matching record, or None. It requests one row.
  • count() — The number of matching rows. It requests one row and reads total.

Create records#

POST …/data/
(
client.database.from_("tasks")
.create([
{"title": "Write release notes", "done": False},
{"title": "Ship docs", "done": False},
])
.execute()
)

body is a dict or a list of dicts. Returns the response envelope; data is a list of the created records.

Upsert records#

POST …/data/upsert/?unique_fields=title
(
client.database.from_("tasks")
.upsert([{"title": "Ship docs", "done": True}], unique_fields=["title"])
.execute()
)

Inserts records, or updates those whose unique_fields match, by default the primary key. Returns the envelope; data is {"records": [...], "count": N}.

Update records#

PATCH …/data/ID/
client.database.from_("tasks").get("TASK_ID").update({"done": True}).execute()

After get(id), update(body) changes that record. Pass a list of records, each with its primary key, to update several with PATCH …/data/. Returns the response envelope.

Delete records#

DELETE …/data/ID/
client.database.from_("tasks").delete("TASK_ID").execute()
  • delete(id) — Deletes one record and returns {}.
  • delete(ids) or bulk_delete(ids) — Deletes records by ID, with DELETE …/data/?ids=.
  • delete_filtered() — Deletes every record that matches the query's filters, with DELETE …/data/?filter=. Raises ValueError when no filter is set.

Bulk and filtered deletes return {"deleted_count": N, "message": ...}. When nothing matches, delete_filtered() returns the envelope, with the count in deleted_count inside data.

Filtered deletes are permanent

delete_filtered() removes every matching record in one request and can't be undone. Run the same filters as a read first and check total.

Graphs and edges#

GET …/data/ID/?include=descendants&depth=2&format=tree
(
client.database.from_("folders")
.get("ROOT_ID")
.include("descendants")
.depth(2)
.format("tree")
.execute()
)
  • include(direction) — "descendants", "ancestors", or "both".
  • depth(n) — The maximum traversal depth.
  • format(format_type) — "flat", "tree", or "graph".
  • types(relationship_types) — The relationship types to follow.

edges() targets the table's edge table, TABLE_edges; create(), update(), and delete() then act on edges. See Graphs and hierarchies.

Module shortcuts#

For one-off calls, the module has shortcuts that return the record itself rather than the envelope:

client.database.get("tasks", "TASK_ID")
client.database.create("tasks", {"title": "Ship docs", "done": False})
client.database.update("tasks", "TASK_ID", {"done": True})
client.database.delete("tasks", "TASK_ID")
  • get(table, record_id) — The record.
  • create(table, data) — The created records, as a list.
  • update(table, record_id, data) — The updated record. With a list of records instead of record_id, updates them all and returns {"records": [...], "count": N}.
  • delete(table, record_id=None, *, ids=None, filter=None) — Pass exactly one of the three.

Storage#

client.storage.from_(bucket) selects a bucket; the methods below run on it. Object paths may contain /. Request paths are relative to /api/apps/APP_SLUG/storage/buckets/BUCKET/objects/.

List objects#

GET …/objects/
(
client.storage.from_("documents")
.filter(search="q3", mimetype="application/pdf", page=1, page_size=50)
.list()
)

filter() takes page, page_size, search, mimetype, mimetype_category, visibility, ordering, and any other storage filter as a keyword. Returns the response envelope, with the objects in data and the count in total.

Browse folders#

GET …/objects/browse/
listing = client.storage.from_("documents").browse(prefix="reports/", sort="name")
print(listing["folders"], listing["objects"], listing["has_next"])

Takes prefix, page (default 1), page_size (default 50, up to 100), sort, and order. Returns the folders and files directly under prefix.

Upload files#

Uploads up to 100 files, 100 MB in total, in one request.

POST …/objects/batch-upload/
with open("q3.pdf", "rb") as report:
result = client.storage.from_("documents").upload(
files=[("q3.pdf", report)],
paths=["reports/q3.pdf"],
metadatas=[{"owner": "finance"}],
)
print(result["uploaded_count"], result["failed"])
  • files (required) — Each entry is (filename, file_obj) or (filename, file_obj, content_type). Without a content type, the SDK guesses it from the filename.
  • paths (required) — The object path for each file, in the same order.
  • metadatas — Metadata for each file, in the same order.

Returns uploaded_count, failed_count, successful, and failed. An upload in which some files fail doesn't raise: check failed_count after every upload.

Download a file#

GET …/objects/PATH/
client.storage.from_("documents").download("reports/q3.pdf")

Returns the file as bytes. A failed download raises the matching SDK error.

Update metadata#

PATCH …/objects/PATH/
client.storage.from_("documents").update(
"reports/q3.pdf",
metadata={"owner": "finance", "status": "final"},
)

metadata replaces the object's metadata. Returns the object. An object's visibility always follows its bucket; TaruviBase ignores a visibility passed here.

Delete objects#

POST …/objects/batch-delete/
result = client.storage.from_("documents").delete(["reports/q1.pdf", "reports/q2.pdf"])
print(result["deleted_count"], result["failed"])

Returns deleted_count and failed. Objects that are missing, or that a policy doesn't let the caller delete, are listed in failed rather than raising.

Copy and move objects#

POST …/objects/copy/
client.storage.from_("documents").copy_object(
"reports/q3.pdf",
"archive/2026/q3.pdf",
)
  • copy_object(source_path, destination_path, destination_bucket=None) — Copies the object, into another bucket when destination_bucket is set. Returns the new object.
  • move_object(source_path, destination_path) — Moves or renames it within the bucket, with POST …/objects/move/. Returns the moved object.

For buckets backed by SharePoint, view_access(file_path) and edit_access(file_path) return a link that opens an Office file, with GET …/objects/PATH/view/ or …/edit/. Editing always needs a signed-in user.

Manage buckets#

The module itself manages buckets: list_buckets(), create_bucket(name, …), get_bucket(slug), update_bucket(slug, …), and delete_bucket(slug). delete_bucket() also deletes every object in the bucket. See Manage buckets.

Functions#

client.functions runs your app's functions and reads their runs.

Run a function#

POST /api/apps/APP_SLUG/functions/SLUG/execute/
client.functions.execute("send-report", {"month": "2026-09"})
  • function_slug (required) — The function's slug.
  • params — The function's input.
  • is_async — True queues the run and returns right away; False waits for the result. Leave it unset to use the function's own mode.
  • timeout — Seconds to wait for this call, overriding the client's timeout.

Returns the response envelope: data is the function's return value, and invocation describes the run.

Get a queued run's result#

A queued run returns at once with an empty data. Read the result later with its task ID:

GET /api/result/TASK_ID/
run = client.functions.execute("send-report", {"month": "2026-09"}, is_async=True)
task_id = run["invocation"]["celery_task_id"]
client.functions.get_result(task_id)

See Execute functions.

List functions and runs#

  • list(*, limit=100, offset=0) — The app's functions, with GET /api/apps/APP_SLUG/functions/.
  • get(function_slug) — One function.
  • list_invocations(*, function_slug=None, status=None, limit=100, offset=0) — Past runs, with GET /api/invocations/.
  • get_invocation(invocation_id) — One run.

These need a TaruviBase Console user with an elevated role; app users can only run functions.

Secrets#

client.secrets reads secrets that the caller may see. When app is omitted, the client's app_slug is used.

Get a secret#

GET /api/secrets/KEY/
secret = client.secrets.get("app_config")
print(secret["value"])
  • key (required) — The secret's key.
  • app — An app slug, to read that app's secret.
  • tags — Tags the secret must have; if it has none of them, the call raises NotFoundError.

Returns the secret: its key, value, tags, and secret_type.

Get or list several secrets#

GET /api/secrets/?keys=app_config,feature_flags
client.secrets.list(keys=["app_config", "feature_flags"])

With keys, data maps each key to its value, or to {value, tags, secret_type} with include_metadata=True; keys that are missing or not readable are left out. Without keys, list() returns a page of secrets filtered by search, app, tags, and secret_type, with page and page_size (up to 100). See Read secrets from code.

Policy#

client.policy asks TaruviBase what the caller may do. Checks always run as the client's caller.

Check resources#

POST /api/apps/APP_SLUG/check/resources/
result = client.policy.check_resources([
{
"resource": {"kind": "datatable:tasks", "id": "TASK_ID", "attr": {}},
"actions": ["update", "delete"],
}
])
can_delete = result["results"][0]["actions"]["delete"] == "EFFECT_ALLOW"

Returns one result per entry, mapping each action to EFFECT_ALLOW or EFFECT_DENY. Pass aux_data by keyword: the deprecated principal argument comes before it, and TaruviBase rejects any principal with a 400.

Filter by permission#

POST /api/apps/APP_SLUG/check/resources/
tables = [
{"kind": "datatable:tasks", "id": "TASK_ID_1"},
{"kind": "datatable:tasks", "id": "TASK_ID_2"},
]
client.policy.filter_allowed(tables, ["update"])
  • filter_allowed(resources, actions) — The resources on which every action is allowed.
  • get_allowed_actions(resource, actions=None) — The allowed action names for one resource. actions defaults to read, write, create, update, and delete.

Both take resources as {"kind": ..., "id": ...}. See Runtime permission checks.

Users#

client.users manages your site's users. Any signed-in caller can list and read them. Creating, updating, and deleting users needs organization access (an organization owner, admin, or member, or an API key one of them created) or a Super Admin app role in one of the site's apps. Anyone else gets 403 with code FORBIDDEN, raised as AuthorizationError. A Super Admin app role can't change superusers or members of your organization. See Site users.

List and get users#

GET /api/users/
result = client.users.list(search="ada", is_active=True, page_size=20)
for user in result["data"]:
print(user["username"])

list(**filters) takes search, is_active, is_superuser, is_deleted, is_cloud_user, roles (comma-separated slugs), ordering, page, and page_size, and returns the envelope with data and total. Without filters, the list returns active users that aren't deleted, and leaves out TaruviBase cloud users and superusers; a filter you pass replaces its default. get(username) returns one user, with GET /api/users/USERNAME/.

Create, update, and delete users#

POST /api/users/
client.users.create({
"username": "ada",
"email": "[email protected]",
"password": "CHOOSE_A_PASSWORD",
"confirm_password": "CHOOSE_A_PASSWORD",
"first_name": "Ada",
"last_name": "Lovelace",
})
  • create(data) — Creates a user. username, email, password, confirm_password, first_name, and last_name are required.
  • update(username, data) — Changes only the fields you pass, with PUT /api/users/USERNAME/. TaruviBase ignores is_staff, is_superuser, and is_cloud_user on create and update.
  • delete(username) — Soft-deletes the user, with DELETE /api/users/USERNAME/: TaruviBase marks them deleted and inactive.
  • apps(username) — The user's apps, with GET /api/users/USERNAME/apps/.

Assign and revoke roles#

POST /api/assign/roles/
result = client.users.assign_roles(
roles=["editor"],
usernames=["ada"],
expires_at="2026-12-31T23:59:59Z",
)
print(result["message"])
  • assign_roles(roles, usernames, expires_at=None) — Gives every user in usernames every role in roles, until expires_at when it's set.
  • revoke_roles(roles, usernames) — Removes them, with DELETE /api/revoke/roles/.

Both need organization access (an organization owner, admin, or member, or an API key one of them created), and take up to 100 roles and 100 usernames. Assignments that already exist, or when revoking don't exist, are skipped and count as success. Returns a message, plus data.failures when some changes failed.

Read and update preferences#

PUT /api/users/me/preferences/
client.users.update_preferences({"theme": "dark", "timezone": "Europe/London"})

get_preferences() reads the caller's preferences with GET /api/users/me/preferences/. update_preferences(data) changes only the fields you pass: date_format, time_format, timezone, theme, or widget_config.

App, settings, and analytics#

Read app roles and settings#

GET /api/apps/APP_SLUG/roles/
client.app.roles()

app.roles() lists the app's roles, and app.settings() reads its settings with GET /api/apps/APP_SLUG/settings/.

Read public site settings#

GET /api/settings/metadata/
client.settings.get()

Works without a credential. Returns the site's domain and its public settings, such as theme and branding.

Run a saved query#

POST /api/apps/APP_SLUG/analytics/queries/SLUG/execute/
client.analytics.execute("revenue-by-month", {"year": 2026})

Returns the response envelope, with the query's result in data. See Execute saved queries.

A run that fails raises ValidationError with status 400 and code "BAD_REQUEST"; the cause is in the message. See Error categories.

Errors#

Import every class from taruvi. API errors carry message, status_code, code, detail, and details, and to_dict() returns them as a dict. NotAuthenticatedError carries message, status_code, and the response's code ("UNAUTHORIZED") and detail; its message says to sign in first.

ClassRaised for
ValidationError400; details holds field errors
AuthenticationError401 with a credential
NotAuthenticatedError401 from a client with no credential
BillingError402, 429, or 503 when billing blocked the request
AuthorizationError403
NotFoundError404
ConflictError409
RateLimitError429
ServerError500
ServiceUnavailableError503
GatewayTimeoutError504
APIErrorAny other 4xx or 5xx
TimeoutErrorNo response within timeout, after any retries
ConnectionErrorThe connection failed or dropped, after any retries
ResponseErrorThe response wasn't valid JSON
ConfigurationErrorapi_url or app_slug is missing

TaruviError is the base class of every SDK error. APIError is the base of every error with an HTTP status, and NetworkError of TimeoutError and ConnectionError. NotAuthenticatedError is a subclass of AuthenticationError.

BillingError means the organization's billing blocked the request: the account isn't active (account_suspended), the plan's usage for module is used up for this period (product_suspended), or billing status couldn't be read (gate_unavailable). Only gate_unavailable is worth retrying; check retryable.

ValueError is raised for invalid arguments, such as update() without a record ID or delete_filtered() without a filter.