ouro-py
The official Python SDK for Ouro.
Use it to work with datasets and files, publish results, collaborate with teams, and run APIs shared on Ouro.
Install
pip install ouro-py
ouro-py requires Python 3.10 or later.
Quickstart
Create a Personal Access Token in your Ouro settings, then export it:
export OURO_API_KEY="your-api-key"
Create a client and start using the API:
from ouro import Ouro
ouro = Ouro()
dataset = ouro.datasets.create(
name="experiment-results",
visibility="private",
data=[
{"sample": "A", "score": 0.82},
{"sample": "B", "score": 0.91},
],
)
results = ouro.datasets.query(dataset.id)
print(results)
The client reads OURO_API_KEY from your environment or a .env file. You can also pass it
directly with Ouro(api_key="...").
Common workflows
Find assets
Datasets, posts, files, services, routes, and quests are all assets. Search across them with
ouro.assets:
results = ouro.assets.search(
"battery materials",
asset_type="dataset",
scope="global",
limit=10,
)
asset = ouro.assets.retrieve(results[0].id)
Search scope can be personal, org, global, or all.
Returned objects
Every method returns a typed model, so fields are attributes (asset.name,
request.id) and your editor can complete them. List endpoints return a
Page, which iterates, indexes, and measures like a list and also carries
has_more and total:
page = ouro.posts.list(limit=50)
for post in page:
print(post.name)
if page.has_more:
next_page = ouro.posts.list(limit=50, offset=page.offset + len(page))
Fields the API adds after your SDK version shipped are still readable as
attributes. Call model_dump() on any object for a plain dict.
Query a dataset
Dataset queries return pandas DataFrames by default:
df = ouro.datasets.query(dataset_id)
summary = ouro.datasets.query(
dataset_id,
"""
select category, avg(score) as mean_score
from {{table}}
group by category
""",
)
SQL queries are read-only. Use {{table}} as the dataset table placeholder.
Save a dataset view
A view is a saved chart. Pass a prompt and the API generates the SQL and chart config — you rarely write those by hand:
view = ouro.datasets.create_view(
dataset.id,
name="Score by sample",
prompt="Bar chart of mean `score` by `sample`, highest first.",
)
You can still pass sql_query and config if you already have them. List,
update, and delete with list_views, update_view, and delete_view. Embed
the chart in a post with
display_config={"visualizationId": str(view.id)} on new_inline_asset.
Upload a file
file = ouro.files.create(
name="Crystal structure",
file_path="./structure.cif",
visibility="private",
)
Download any supported asset through the shared asset interface:
download = ouro.assets.download(file.id, output_path="./downloads")
print(download.path)
Publish a post
Pass markdown directly or build richer content with the editor:
editor = ouro.posts.Editor()
editor.new_header(level=1, text="Experiment summary")
editor.new_paragraph(text="The best sample reached a score of 0.91.")
editor.new_inline_asset(dataset.id, asset_type="dataset", view_mode="preview")
post = ouro.posts.create(
name="Experiment summary",
content=editor,
visibility="private",
)
You can also use content_markdown="..." or content_path="./report.md".
Run an API
Ouro services expose individual endpoints as routes:
action = ouro.routes.execute(
"organization/route-name", # a route slug or UUID
body={"text": "hello"},
)
print(action.status)
print(action.final_data)
Routes can take Ouro assets directly:
action = ouro.routes.execute(
route_id,
input_assets={"structure": file.id},
)
Synchronous and asynchronous routes use the same interface. Pass wait=False to return
immediately, then use ouro.routes.poll_action(action.id) when you are ready for the result.
ouro.routes.list_my_actions(status=["queued", "in-progress"]) finds everything you still
have running, across every route.
Follow a run from the shell
The ouro command (installed with the package, authenticated by OURO_API_KEY) blocks until
an action finishes, so a script or a coding agent can run it in the background and be told
when the result is ready instead of polling:
ouro action wait <action-id> # exit 0 success, 1 failed, 124 still running after --timeout
ouro action get <action-id> # current status and outputs
ouro action list --running # your queued and in-progress runs
Add --json to any of them for machine-readable output.
API overview
Resources are organized under one client:
| Namespace | Use it for |
|---|---|
ouro.assets |
Search, retrieve, share, download, and inspect lineage |
ouro.datasets |
Create, query, update, and visualize tabular data |
ouro.files |
Upload, retrieve, update, and search files |
ouro.posts |
Publish markdown and embedded assets |
ouro.routes |
Execute APIs and inspect actions |
ouro.services |
Publish an API from an OpenAPI specification |
ouro.quests |
Create work, submit entries, and review results |
ouro.organizations / ouro.teams |
Manage workspaces, channels, and membership |
ouro.comments / ouro.conversations |
Discuss assets and send messages |
ouro.users / ouro.notifications |
Work with profiles and notifications |
ouro.money |
Check balances, transactions, and paid access |
See the REST API reference for the underlying API.
Organizations, teams, and visibility
Every asset belongs to an organization and a team. When creating one, pass org_id and team_id
to choose where it appears:
dataset = ouro.datasets.create(
name="shared-results",
data=rows,
visibility="public",
org_id=org_id,
team_id=team_id,
)
If you omit them, Ouro uses your global organization's catch-all team.
Working in one organization
Most sessions stay in one organization. Pin the client to it and everything you create goes there:
ouro = Ouro(organization="acme-lab") # name or UUID; or set OURO_ORG_ID
ouro = Ouro(organization="acme-lab", team=team_id) # or OURO_TEAM_ID
post = ouro.posts.create(name="Run notes", content_markdown="...") # lands in acme-lab
New assets go to team when a call doesn't name one, and to the organization's default team
when no team is pinned. A pinned client refuses to create in, or move an asset to, any other
organization (OuroError); reads are not restricted. Switch with
ouro.use_organization("other-org"), unpin with ouro.use_organization(None), and pass
organization="" to ignore OURO_ORG_ID.
Visibility follows the team
A team is the boundary for what's in it. Everything in an internal (organization-only) team
stays inside the organization: public and monetized are refused there with
PermissionDeniedError. Leave visibility unset and a new asset takes the team's audience,
public in a public team and organization in an internal one. To publish internal work,
move it to a public team:
ouro.posts.update(post_id, team_id=public_team_id, visibility="public")
Whether an organization has public teams, and who may move work into them, is the organization's own setting.
Visibility can be public, organization, private, or monetized. Monetized assets also take a
monetization model and a price in price_currency ("usd" in dollars, "btc" in sats):
ouro.posts.update(post_id, visibility="monetized", monetization="pay-to-unlock",
price=0.50, price_currency="usd")
ouro.routes.update(route_id, visibility="monetized", monetization="pay-per-use",
cost_accounting="fixed", unit_cost=10, price_currency="btc")
An asset can be sold in both currencies, each at its own price. Set price_usd and
price_sats (or unit_cost_usd and unit_cost_sats on routes); buyers pick which to pay in,
and price_currency is what's charged when they don't. Pass 0 to stop selling in a currency.
ouro.posts.update(post_id, visibility="monetized", monetization="pay-to-unlock",
price_usd=0.50, price_sats=500)
ouro.routes.update(route_id, unit_cost_usd=0.10, unit_cost_sats=100)
ouro.money.unlock_asset("post", post_id, currency="btc")
ouro.routes.execute(route_id, body={...}, currency="usd")
Private assets remain private until you share them explicitly:
ouro.assets.share(asset_id, user_id, role="read")
Licensing and attribution
Set an asset's reuse terms with license_id, and record where the work came from with
the top-level attribution field. These fields are consistent across asset types and separate
from type-specific metadata:
attribution = {
"originality": "derivative",
"github_url": "https://github.com/example/project",
"doi_url": "https://doi.org/10.1234/example",
"relation_type": "IsDerivedFrom",
}
dataset = ouro.datasets.create(
name="published-results",
visibility="public",
data=rows,
license_id="CC-BY-4.0",
attribution=attribution,
)
originality can be original, derivative, or third-party. Attribution can also include
paper_url and external_url. Use relation_type to describe how the Ouro asset relates to the
linked work: IsSupplementTo, IsDerivedFrom, References, IsVariantFormOf, or
IsIdenticalTo.
Pass the same top-level fields when creating another asset type:
service = ouro.services.create(
name="published-model-api",
base_url="https://api.example.com",
license_id="Apache-2.0",
attribution={
"originality": "third-party",
"github_url": "https://github.com/example/model",
"paper_url": "https://arxiv.org/abs/0000.00000",
},
)
Use the license that applies to the asset type and only publish third-party or derivative work when its terms permit redistribution.
Configuration
| Variable | Default | Purpose |
|---|---|---|
OURO_API_KEY |
required | Personal Access Token |
OURO_BACKEND_URL |
https://api.ouro.foundation |
Ouro API base URL |
OURO_ORG_ID |
unset | Pin the client to this organization (UUID or name) |
OURO_TEAM_ID |
unset | Team new assets go to when pinned (default: the org's default team) |
For local development:
export OURO_BACKEND_URL="http://localhost:8003"
You can also pass api_key, base_url, organization, and team directly to Ouro(...).
Error handling
All SDK exceptions inherit from OuroError:
from ouro import NotFoundError, OuroError
try:
asset = ouro.assets.retrieve(asset_id)
except NotFoundError:
print("Asset not found")
except OuroError as exc:
print(f"Ouro request failed: {exc}")
Development
git clone git@github.com:ourofoundation/ouro-py.git
cd ouro-py
pip install -e .
pytest
tests/integration exercises the SDK against a live backend. It creates real
assets (named sdk-it-<run>-...) and deletes them when the session ends, and
it only runs when an API key is set. A second key enables the sharing,
membership, and multi-user tests. The suite refuses non-local backends unless
OURO_TEST_ALLOW_REMOTE=1.
export OURO_TEST_API_KEY=... # primary user
export OURO_TEST_API_KEY_2=... # optional second user
export OURO_TEST_BASE_URL=http://localhost:8003
pytest tests/integration
Route tests register a mock OpenAPI service on 127.0.0.1, so the backend
must be able to reach the machine running the tests. Known backend bugs are
marked xfail(strict=True); when one starts passing, remove its marker.
Questions and ideas are welcome in GitHub Discussions.
License
MIT
Metadata
Release files for ouro-py 2.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ouro_py-2.4.0.tar.gz | 92.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ouro_py-2.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 209.9 kB
Release files / ouro_py-2.4.0.tar.gz
| Download URL | ouro_py-2.4.0.tar.gz |
|---|---|
| Size | 92.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
beec49ef9442fd4ade5bc9d9f4749f4ebf3c80513cc80761db527b00c8755ed4
|
|
BLAKE2b-256 checksum How to use checksums |
1de7e0b5219ba2dcbc7a4958384cea52116d470b1ef2b46bbaed1fb00cce5287
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / ouro_py-2.4.0-py3-none-any.whl
| Download URL | ouro_py-2.4.0-py3-none-any.whl |
|---|---|
| Size | 117.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f7eae8a3f3fb797716b50bde6754090528908ffdac55e6719f851ad2b7ff11a
|
|
BLAKE2b-256 checksum How to use checksums |
0b07740956d8d63a68ecd5ca3532528cfa540bb6f518dbbc206af5d1a7581c84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log