Skip to content

Python SDK

rho-store reads and writes tables from Python: the scripts, notebooks and jobs around your agents.

pip install "rho-store[pandas]" # or [polars]
export RHO_API_KEY=rho_sk_… # an API token

Make the token on the API tokens page of the rho app: see API tokens.

import rho
rho.put(df, "research/competitors") # a new version; columns keep their types
rho.put(new, "research/competitors", mode="upsert", key="company", message="weekly refresh")
df = rho.get("research/competitors") # the latest; version=3 for an earlier one
df = rho.query('SELECT category, count(*) AS n FROM "research/competitors" GROUP BY 1')
rho.history("research/competitors") # who wrote what, when
rho.restore("research/competitors", version=3) # as a new version
url = rho.share("research/competitors") # makes it public: a link anyone can open

put takes a pandas or Polars DataFrame, an Arrow table or a list of dicts. get and query give pandas by default; pass as_="polars", "arrow" or "rows" for another.

Data travels as Parquet, never JSON, so types come back as they went: whole numbers stay whole (nullable ones become Int64), timestamps keep nanoseconds, decimals their scale. A few things are kept differently:

  • a time zone becomes UTC, the same instant;
  • a category becomes text;
  • a DataFrame’s default index isn’t kept, and a named one becomes a column;
  • durations are refused, with a hint.
  • replace, the default: the data becomes the table.
  • append: adds the rows.
  • upsert: updates the rows whose key matches, and adds the rest.
  • create: fails if the table exists.

Pass run="nightly-2026-10-29" to label a job’s writes in the history.

async with rho.AsyncClient() as client:
await client.put(df, "runs/evals", mode="append", run=run_id)
df = await client.query('SELECT model, avg(score) FROM "runs/evals" GROUP BY 1')

rho.Client and rho.AsyncClient have the same methods. Pass api_key= and url= to choose them yourself; RHO_URL points the module’s functions at another server.

What rho decides about data it’s given comes as a rho.RhoWarning, and the write is made. Column names are compared without case, so A and a in one frame are refused.

Errors are rho.RhoError, with kind (not_found, exists, schema_conflict, …) and a hint saying what to do.