Python SDK
dalea by example — sync + async across all four domains.
dalea is the official public Python SDK — a backend/agent-focused client
with a pooled httpx transport and sync + async on one resource tree,
covering the data, documents, inventory, search, and storage domains. It is
contract-aligned with the TypeScript @dalea/sdk:
both are generated from the same public OpenAPI subset. This page is the
by-example tour; shared concepts live in the
Dalea SDK overview, and every operation in the
API reference shows a ready-to-copy dalea
snippet.
Install
pip install dalea
Python 3.11+. Ships type hints (PEP 561), so editors and type checkers see the full surface.
Quickstart
Set DALEA_API_KEY (a workspace-scoped key) and construct a client. Use it as a
context manager so the HTTP pool is cleaned up.
from dalea import DaleaClient
with DaleaClient(
base_url="https://dalea.app",
api_key="dalea_...", # or omit → read from DALEA_API_KEY
) as dalea:
env = dalea.data.environments.list()["environments"][0]
print(env["name"])
The API key already pins the workspace, so that's all you need.
Every resource method returns the parsed JSON response (a dict / list),
exactly like the TS SDK's runtime return. Read it with dictionary access:
there are no attribute-style response objects. Three binary reads are the
exception and return raw bytes: storage.files.download,
storage.files.preview, and data.tables.stream_objects (parse the last with
dalea.parse_ndjson()). A 204 returns {}.
Async
The same API, awaited — AsyncDaleaClient shares the resource tree:
import asyncio
from dalea import AsyncDaleaClient
async def main():
async with AsyncDaleaClient(
base_url="https://dalea.app", api_key="dalea_..."
) as dalea:
table = await dalea.data.tables.get("table-uuid")
asyncio.run(main())
Credentials
Auth resolves in this precedence: explicit credential → api_key → the
DALEA_API_KEY environment variable. There is no session-cookie option — that
is a browser concern, so it exists only in the TypeScript SDK. See the
overview for when to use each
credential, and Authentication for obtaining them.
import os
from dalea import DaleaClient, OAuthPkceCredential, OAuthClientCredentialsCredential
# API key — the default for scripts and backends. With DALEA_API_KEY set:
dalea = DaleaClient(base_url="https://dalea.app")
# OAuth 2.1 + PKCE — acting on behalf of a user (auto-refresh on 401):
cred = OAuthPkceCredential.exchange_code(
token_endpoint="https://dalea.app/api/v1/auth/oauth2/token",
client_id="my-app",
code=code, # from the ?code=… redirect
code_verifier=code_verifier,
redirect_uri="https://myapp.example/callback",
on_tokens=lambda tokens: persist(tokens), # called on every rotation
)
dalea = DaleaClient(base_url="https://dalea.app", credential=cred)
# OAuth 2.1 client-credentials — machine-to-machine:
m2m = DaleaClient(
base_url="https://dalea.app",
credential=OAuthClientCredentialsCredential(
token_endpoint="https://dalea.app/api/v1/auth/oauth2/token",
client_id="my-service",
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
),
)
Workspace context
API keys and OAuth credentials are bound to a workspace when created, so you
normally never set one. For credentials that don't pin a workspace, the client
sends an X-Workspace header: set a default (workspace=... or
dalea.set_workspace(id)), or override per call — the per-call value wins:
dalea.data.tables.get("t1", workspace="other-workspace-id")
Data
Walk your workspace's data: an environment → its first table → that table's size.
env = dalea.data.environments.list()["environments"][0]
table = dalea.data.environments.list_tables(env["id"])["tables"][0]
page = dalea.data.tables.list_objects(table["id"], {"limit": 50})
print(f'{env["name"]} → {table["name"]}: {page["total"]} objects')
Documents
Create a document, write its body as markdown, and read the whole document back as markdown:
doc = dalea.documents.create({
"workspaceId": "ws_123",
"title": "Assay run notes",
})
dalea.documents.markdown.append(doc["id"], {
"markdown": "## Results\n\nAll 96 wells within expected range.",
})
outline = dalea.documents.markdown.outline(doc["id"])
print(f'{outline["blockCount"]} blocks:\n{outline["markdown"]}')
The markdown sub-resource also supports insert, update_block,
replace_section, delete_blocks, and parse. See the
API reference for each.
Inventory
Read the item-type catalogue, find a container by barcode, and log consumption from an item. Item types are read-only here: they are data tables, designed in the environment designer.
item_types = dalea.inventory.item_types.list()["itemTypes"]
container = dalea.inventory.containers.get_by_barcode("FRZ-A-001")
dalea.inventory.consumption.consume("item-uuid", {
"quantity": 2.5,
"reason": "Assay run 42",
})
Placements handle physical movement — check_in, batch_check_out, move,
and place_object under dalea.inventory.placements.
Search
Unified search across documents, files, data objects, and result schemas:
page = dalea.search.search({"query": "aspirin", "types": "document,data_object", "limit": 25})
# Scope data-object results to one or more tables (e.g. the same table
# name across several environments) — `table_ids` joins into the wire param:
scoped = dalea.search.search({"query": "batch-42", "table_ids": [table_a["id"], table_b["id"]]})
# Auto-paged: walk every result across pages.
for hit in dalea.search.search_all({"query": "aspirin"}):
print(hit["type"], hit["name"])
Storage
Upload a file and hand out a short-lived presigned download URL:
# Multipart from raw bytes:
with open("plate-map.csv", "rb") as fh:
uploaded = dalea.storage.upload.file(fh, filename="plate-map.csv")
# Or JSON with base64 content:
dalea.storage.upload.base64({
"filename": "report.pdf",
"mimeType": "application/pdf",
"data": base64_payload,
})
url = dalea.storage.files.get_url(uploaded["file"]["id"])["url"]
Presigned URLs are short-lived — fetch a fresh one per download rather than storing it.
Pagination
List methods return one page. Five surfaces have an auto-paging companion that
walks every item across pages, a sync generator on DaleaClient and an
async generator on AsyncDaleaClient: data.tables.iterate_objects,
data.tables.iterate_archived_objects, data.tables.search_objects_all,
data.saved_queries.iterate, and search.search_all.
# sync — on a DaleaClient
for obj in dalea.data.tables.iterate_objects(table_id, {"limit": 200}):
... # every object, across all pages
# async — the same method on an AsyncDaleaClient
async for obj in dalea.data.tables.iterate_objects(table_id, {"limit": 200}):
...
Everything else returns a single page. Cursor-paged lists (documents, files)
return a nextCursor you pass back as cursor; offset-paged lists take
limit and offset.
Errors
Non-2xx responses map to a typed error hierarchy. Throttling (429) and
transient 503s are retried automatically with exponential backoff + jitter,
honoring Retry-After. A 429 is rejected before the handler runs, so
replaying is safe: any method is retried, writes included, unless you pass
retry_writes=False. A 503 may come from inside a handler that already
committed a write, so it is retried for GETs only and never replays a write,
whatever retry_writes says. Multipart uploads are never replayed. A
DaleaRateLimitError surfaces only once retries are exhausted and carries
retry_after_seconds. Tune with max_retries (default 2), retry_writes
(default True), retry_base_delay, and max_retry_delay.
from dalea import DaleaNotFoundError, DaleaRateLimitError
try:
dalea.data.tables.get(table_id)
except DaleaNotFoundError:
... # 404
except DaleaRateLimitError as err:
print(err.retry_after_seconds) # 429
The public SDK never deletes an entity. Lifecycle is reversible:
archive / restore across the data domain, and
documents/inventory/storage follow the same no-hard-delete posture. The one
exception is documents.markdown.delete_blocks, which permanently
removes blocks from a document body and requires an audit reason.
Resources
client.data.*—environments(incl. data import/export, staging views),tables,columns,objects,query,saved_queries,results,schema,naming_schemes,import_mappings.client.documents.*—list/create/get/update, plusmarkdown.*for the block round-trip.client.inventory.*—item_types,lots,containers,placements,items,consumption,activity.client.search.*—search, plus the auto-pagedsearch_all.client.storage.*—files,upload,usage.
The client covers exactly the public operations in the contract — the API reference is the authoritative catalogue.