Skip to main content

ouro-py

Version Python License: MIT

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",
)

When the bytes live somewhere this client can't read (an agent's sandbox, a browser), reserve a signed upload URL, have the holder PUT the bytes to it, and create the file from the upload:

upload = ouro.files.create_upload_url("structure.cif")   # upload_url, method, headers, expires_in
file = ouro.files.create(name="Crystal structure", upload_id=upload["upload_id"])

Just after upload, Ouro inspects structure files and records what it found in file.metadata: a classification (for example {"kind": "crystal", "inspector": "cif"}) and, for CIFs, a structure summary. These always come from the bytes; values you send for them are ignored.

Download any supported asset through the shared asset interface:

download = ouro.assets.download(file.id, output_path="./downloads")
print(download.path)

Or get a link that downloads it without credentials, for a caller that fetches the bytes itself. Files keep their bytes, datasets download as CSV, and posts as markdown (or format="html"):

link = ouro.assets.create_download_url(file.id)
print(link["download_url"], link["expires_in"])

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.

An API key can itself be bound to one context when it is created. A key bound to an organization pins the client to it automatically and sees only public work elsewhere. A key bound to your personal context can't be pinned to an organization (OuroError) and can't create or change anything in one (PermissionDeniedError). A key that acts everywhere runs in your personal context unless you pin the client.

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, and two bound keys enable the key-context tests. The boundary tests need the primary user to administer an organization the second user is outside of. 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_API_KEY_PERSONAL=...  # optional: primary user's key bound to the personal context
export OURO_TEST_API_KEY_ORG=...       # optional: primary user's key bound to an organization
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.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ouro-py 2.9.0
File Size Uploaded
ouro_py-2.9.0.tar.gz 96.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ouro-py 2.9.0
File Interpreter ABI Platform
ouro_py-2.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 218.1 kB

Release files / ouro_py-2.9.0.tar.gz

Download URL ouro_py-2.9.0.tar.gz
Size 96.7 kB
Tags Source
SHA-256 checksum
How to use checksums
67caab88a032de260d3bf7be2600b3fb007358581ddb9fe34a6f577368fca8f7
BLAKE2b-256 checksum
How to use checksums
3730cedc76ee812cbbf07b7e81259ce80ea26132f159b532a794f0d82bdbd350
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 17, 2026.

Transparency log

Release files / ouro_py-2.9.0-py3-none-any.whl

Download URL ouro_py-2.9.0-py3-none-any.whl
Size 121.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b761cf53f55e377bafd184924b0732b54ca57c06c29f88626cf86299b2e2bd87
BLAKE2b-256 checksum
How to use checksums
58d7562b7af1772ef30557d7bc7e8a14e85a36650c3bd92bf75d9116339feeb3
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 17, 2026.

Transparency log

Release history Release notifications | RSS feed

2.10.0

2 release files

This release

2.9.0 This release

2 release files

2.8.0

2 release files

2.6.0

2 release files

2.5.1

2 release files

2.5.0

2 release files

2.4.1

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.1.6

2 release files

2.1.5

2 release files

2.1.4

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.0

2 release files

0.11.9

2 release files

0.11.8

2 release files

0.11.4

2 release files

0.11.3

2 release files

0.11.2

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.8

2 release files

0.10.7

2 release files

0.10.6

2 release files

0.10.5

2 release files

0.10.4

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.13

2 release files

0.3.12

2 release files

0.3.11

2 release files

0.3.10

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.18

2 release files

0.2.17

2 release files

0.2.16

2 release files

0.2.15

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page