---
title: "Python SDK for the Ouro API"
description: "Learn how to interact with Ouro from Python."
date: "2025-07-14"
last_updated: "2026-05-23"
---

Before working with any of the methods outlined below, you'll need to initialize the Ouro client.
Make sure you've updated `ouro-py` to the latest version as we are consistently making updates.
More details can be found in the [quickstart](/docs/developers/quickstart).

```python showLineNumbers
import os
from ouro import Ouro

ouro = Ouro(api_key=os.environ.get("OURO_API_KEY"))
```

## Assets

Ouro assets include files, datasets, services, routes, posts, quests, and
conversations. Pass `org_id` and `team_id` when creating assets so they land in
the right team (see [Teams](/docs/platform/teams)).

Skip to one of the sections below:

<div className="mb-4 grid grid-cols-2 gap-x-4">
  <IconLink
    icon="earth"
    href="#files"
    title="Files"
    description="Upload, read, update, and delete files"
  />
  <IconLink
    icon="earth"
    href="#datasets"
    title="Datasets"
    description="Structured tabular data with SQL storage"
  />
  <IconLink
    icon="water"
    href="#services"
    title="Services"
    description="External APIs and route endpoints"
  />
  <IconLink
    icon="air"
    href="#posts"
    title="Posts"
    description="Rich text content with the Editor class"
  />
  <IconLink
    icon="fire"
    href="#quests"
    title="Quests"
    description="Team quests, items, entries, and reviews"
  />
  <IconLink
    icon="doc"
    href="#conversations"
    title="Conversations"
    description="Create threads and exchange messages"
  />
</div>

For every asset on the platform, Ouro stores the following information:

- **ID**
- **Name**
- **Description** (optional)
- **Metadata** (asset specific details, like file size or number of rows)
- **Visibility** (public, private, monetized, or organization)
- **Created at**
- **Updated at**
- **Owner**
- **Organization**
- **Team**
- **Price** (if monetized)

Unless you pass an organization and team, assets are created in your global
organization and default team. Control who can see an asset with `visibility`.

Choosing the right team keeps activity grouped and helps the right people find
the work.

## Files

Files are the most basic asset on Ouro. Any file is fair game, and many file
types have rich visualizations on the web platform.

### List files

Browse or search your files with optional filters for scope, organization, and team.

```python showLineNumbers
# List your recent files
files = ouro.files.list()

# Search with a query
files = ouro.files.list(query="climate data", limit=10)

# Scope to an organization
files = ouro.files.list(org_id=org_id, team_id=team_id)
```

### Create a file

You can upload any file, up to 5GB in size.

```python showLineNumbers
file = ouro.files.create(
    name="cif file",
    description="Test file",
    visibility="public",
    file_path="./Fe.cif",
)
print(file)
```

`file_path` is the path to the file on your local machine.

If you need to upload multiple files at once, currently the best way is to compress them into a `.zip` file and upload that.

If you run into issues uploading large files, you can [upload them using the web interface](/datasets/create). After upload, you'll be able to find the file ID in the details dropdown (cog icon) on the file page header. Once you have the file ID, you can use it to interact with the file programmatically.

### Read a file

```python showLineNumbers
id = '48ec6563-5520-4756-ac7f-b38f4933ac95'
file = ouro.files.retrieve(id)

# file is an object with properties like:
# file.id, file.name, file.description, file.metadata, file.visibility, file.created_at, file.user
```

If you created the file from the SDK, the `ouro.files.create` response will be a file object which has the ID in the `id` field.
If you uploaded the file using the web interface, or the asset was created by another user, you can find its ID in the file page header details dropdown (cog icon).

See the highlighted section in the screenshot below:

![File ID in the details dropdown](/images/docs/find-asset-id.png)

Once you've retrieved a file object with `ouro.files.retrieve`, you can read its data using the `read_data` method.

```python showLineNumbers
file_data = file.read_data()
print(file_data.url)
```

The `read_data` method returns a `FileData` object which has a `url` property that you can use to download the file.

```python showLineNumbers
import requests

url = file_data.url
response = requests.get(url)
print(response.content)
```

### Update a file

To be able to update a file, you must have admin or write permission on the file.
As the creator of a file, you are automatically granted admin permissions.

```python showLineNumbers
file_id = file.id
updated_file = ouro.files.update(
    id=file_id,
    file_path="./Fe2BiNi.cif",
    name="Fe2BiNi (Pmmm) updated"
)
# Returns the updated file object
print(updated_file)
```

You can update the file object with any of asset properties using named parameters. Only `id` is required.
`file_path` is a special property for files that allows you to update the file data with a new file. This is also optional.

### Delete a file

You must have admin permissions on the file to delete it. This will completely remove the file from the platform.

```python showLineNumbers
ouro.files.delete(id=file.id)
```

Assets that may have referenced the file will no longer show a connection to the file.

## Datasets

Datasets are structured tabular assets stored in the `datasets` schema. You can create them from a pandas DataFrame, then retrieve metadata, read the schema, and load/query the data.

### List datasets

Browse or search datasets with optional filters.

```python showLineNumbers
datasets = ouro.datasets.list()
datasets = ouro.datasets.list(query="temperature", scope="org", limit=10)
```

### Create a dataset

Provide a pandas DataFrame and required asset fields. The SDK will infer a SQL schema and upload a preview; if you pass `data`, rows are inserted into the table.

```python showLineNumbers
import pandas as pd

data = pd.DataFrame(
    [
        {"name": "Bob", "age": 30},
        {"name": "Alice", "age": 27},
        {"name": "Matt", "age": 26},
        {"name": "Bobo", "age": 4},
        {"name": "Asta", "age": 15},
    ]
)

dataset = ouro.datasets.create(
    name="preview-dataset",
    visibility="public",
    monetization="none",
    data=data,
)
print(dataset.id)
```

Notes:

- **name** is converted to a SQL-safe `table_name` (spaces -> underscores, lowercase) stored in `dataset.metadata["table_name"]`.
- If you omit `data`, the dataset (asset + empty table) is created without rows.

### Read a dataset

Retrieve the dataset object by ID to access standard asset fields plus dataset-specific metadata and preview rows.

```python showLineNumbers
dataset_id = "0194f68c-b16e-70d3-8ed3-aafa850272ae"
dataset = ouro.datasets.retrieve(dataset_id)
print(dataset.name, dataset.metadata, dataset.preview[:3])
```

### Read schema

Get column definitions for the underlying table.

```python showLineNumbers
columns = ouro.datasets.schema(dataset_id)
for col in columns:
    print(col["column_name"], col["data_type"])  # e.g., age integer, name text
```

### Query data by dataset ID

Fetch the dataset's rows as a pandas DataFrame via the Ouro API. Timestamp and date columns are parsed to pandas types.

```python showLineNumbers
df = ouro.datasets.query(dataset_id)
print(df.head())
```

### Run SQL against a dataset

Pass a SQL string as the second argument to `query()` to run a read-only
PostgreSQL query against the dataset's table. Reference the table as
`{{table}}` - the backend rewrites the placeholder to the fully-qualified
`datasets."..."` name. Read-only is enforced server-side and queries time
out after 10 seconds. Include `LIMIT`/`OFFSET` directly in the SQL.

```python showLineNumbers
# Aggregations
totals = ouro.datasets.query(
    dataset_id,
    "SELECT species, count(*) AS n FROM {{table}} GROUP BY species ORDER BY n DESC",
)

# Time-series buckets
daily = ouro.datasets.query(
    dataset_id,
    "SELECT date_trunc('day', created_at) AS day, count(*) AS n "
    "FROM {{table}} GROUP BY 1 ORDER BY 1",
)

# Sample rows
sample = ouro.datasets.query(dataset_id, "SELECT * FROM {{table}} LIMIT 5")
```

### Update a dataset

Update asset properties and optionally write rows to the table by passing a DataFrame.
Writing new data will replace the existing data in the table.

```python showLineNumbers
updated = ouro.datasets.update(
    dataset.id,
    visibility="private",
)

# Write rows (optional)
data_update = pd.DataFrame([
    {"name": "Charlie", "age": 33},
])
updated = ouro.datasets.update(dataset.id, data=data_update)
```

### Delete a dataset

Requires admin permission on the asset.

```python showLineNumbers
ouro.datasets.delete(dataset.id)
```

Tips:

- **Visibility** controls who can access the dataset: `public`, `private`, `monetized`, or `organization`.
- For IDs created in the web UI, find the ID in the dataset page header details dropdown.

## Services

External APIs on Ouro are organized with two asset types: services and routes.

A service is a collection of routes that all share the same base URL.
Routes are individual endpoints of the API defined by an HTTP method, path,
optional URL and query parameters, and an optional body.

Ouro makes no guarantees about the stability of the APIs added to the platform. Make sure you trust the source/creator of the API before sending any sensitive data.
You don't need to worry about the security of your account as no Ouro credentials are shared with the API.

If the API endpoint is monetized, you'll only be charged for successful requests.

### Create a service

Not yet supported from the SDK. Use the web interface to [create a service](/services/create).

You can learn more about creating services with our guide [How to monetize APIs](/guides/how-to-sell-apis-on-ouro).
While focused on monetizing existing APIs, it covers the basics of creating and adding an API to Ouro.

### Read a service

```python showLineNumbers
service_id = "438e454b-cf9e-40d9-b53d-b9b250087179"
service = ouro.services.retrieve(service_id)
```

Like the file object, using `ouro.services.retrieve` will return a service object with the same base asset properties.

Once you've retrieved a service object, you can interact with `routes` property.

```python showLineNumbers
# Returns a list of Route objects
service_routes = service.read_routes()

# Returns a dictionary with the stored OpenAPI specification as JSON
service_spec = service.read_spec()
print(service_spec['info']['title'])
```

The `read_spec()` method retrieves the stored OpenAPI specification from our database. This is the parsed specification that was either uploaded or fetched from a remote URL when the service was created. The spec is stored as a JSON object and remains stable even if the original remote spec changes, until the service is updated.

You can execute a service's endpoint with the `execute_route` method.

```python showLineNumbers
route = service_routes[0]
action = service.execute_route(
    route.id,
    body={"composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000}
)
response = action.final_data
```

More details on using route functionality is in the routes section.

### Update a service

Not yet supported from the SDK. Use the web interface to update a service.
Navigate the asset and click the edit icon in the asset's header. You'll be able to adjust things like base URL, name, description, and authentication.

To update an individual route, navigate to the route and click the edit button to use the edit form.

### Delete a service

Not yet supported from the SDK. Use the web interface to delete a service.
Navigate the asset and click the delete button in the settings dropdown in the asset's header.

## Routes

Routes are the individual endpoints of a service. Each route represents one
HTTP endpoint of the underlying web API.

### Create a route

Add a route to an existing service with `ouro.routes.create`. Pass the parent
`service_id` and the fields your API expects (method, path, schemas, and so on).
For complex OpenAPI-backed services, creating routes in the [web UI](/services/create)
is often easier.

```python showLineNumbers
route = ouro.routes.create(
    service_id,
    name="transcribe",
    method="POST",
    path="/transcribe",
)
```

### Read a route

To get the details of a route, use the `ouro.routes.retrieve` method.
You can supply either the route ID or the asset identifier, which consists of
the creator and the route name plus method.

The identifier is the section of the URL after `routes/`.
Unless you own the route, use the ID because it remains fixed even if the asset
name changes. When an asset name changes, its identifier and URL also change.

```python showLineNumbers
route_id = "2443b425-a2bf-4a6f-8202-3ba8b36c921f" # CrystaLLM generate route
route = ouro.routes.retrieve(route_id)

# OR

route_identifier = "mmoderwell/post-generate"
route = ouro.routes.retrieve(route_identifier)

# Returns a Route object
```

For file-input routes, inspect the nested route metadata to see compatibility rules:

```python showLineNumbers
route = ouro.routes.retrieve(route_id)

print(route.route.input_type)             # e.g. "file"
print(route.route.input_filter)           # e.g. "audio"
print(route.route.input_file_extensions)  # e.g. ["xy", "xye"]
```

`input_file_extensions` is the preferred field for exact file-format matching.
Older routes may still use the legacy single-value `input_file_extension`.
For routes that declare multiple named inputs or outputs, see the [route input and output assets guide](/guides/route-input-output-assets).

### Update a route

Update route metadata with `ouro.routes.update`. You can pass the route ID or
asset identifier (same as `retrieve`).

```python showLineNumbers
route = ouro.routes.update(
    route.id,
    description="Transcribe audio files to text",
)
```

### Delete a route

Not yet supported from the SDK. Use the web interface to delete a route.

### Execute a route

You can execute a route with the `execute` method. It returns an `Action` object
with the response, status, input/output assets, logs, and action ID. See
[Routes and actions](/docs/concepts/routes#actions) for async runs, webhooks,
and chaining.

```python showLineNumbers
route = ouro.routes.retrieve("mmoderwell/post-generate")

action = route.execute(
    body={
       "composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
    }
)
generation = action.final_data
```

Other supported parameters to `execute` include:

- `query`: query string parameters
- `params`: URL path parameters
- `input_assets`: a dictionary mapping route input names to Ouro asset IDs
- `input_asset`: a legacy single asset object to pass to routes with one asset input

You can chain multiple routes into custom workflows. Many routes create files,
datasets, or posts as outputs. The Python SDK automatically adds saved output
assets to `action.final_data`.

```python showLineNumbers
generation_route = ouro.routes.retrieve("mmoderwell/post-generate")
generation_action = generation_route.execute(
    body={
       "composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
    }
)
generation = generation_action.final_data

# Read the generated file from the action's final data.
file_id = generation["file"]["id"]

prediction_route = ouro.routes.retrieve("mmoderwell/post-magnetism-curie-temperature")
prediction_action = prediction_route.execute(
    input_assets={"file": file_id}
)
prediction = prediction_action.final_data
print(prediction)
```

For routes with multiple named outputs, read outputs by their declared names from
`action.final_data`, `action.output_assets`, or the full action record.

### Read a route's actions

Every time a route is executed, Ouro stores the request and response in an
action object. Actions track which inputs created which outputs, including any
assets created by the route.

```python showLineNumbers
actions = route.read_actions()
# Returns a list of Action objects
print(actions)
```

Actions are especially useful for long-running routes. If a route returns
`202 Accepted`, use the action ID with `ouro.routes.retrieve_action`,
`ouro.routes.poll_action`, or `action.refresh()` to retrieve the result when it
is ready.

## Posts

Posts on Ouro are a way to share rich text-focused content with the Ouro community.
Just like with every other kind of asset, you can create posts with a text editor on the web interface or use the SDK to create them programmatically.

### List posts

Browse or search posts with optional filters.

```python showLineNumbers
posts = ouro.posts.list()
posts = ouro.posts.list(query="machine learning", scope="global", limit=5)
```

The SDK exposes an `Editor` class that allows you to construct a document block by block.
Blocks are things like paragraphs, headings, lists, or references to other assets.

```python showLineNumbers
import pandas as pd

content = ouro.posts.Editor()

content.new_header(level=1, text="Hello World")
content.new_paragraph(text="This is a paragraph")
content.new_code_block(language="python", code="print('Hello, World!')")
content.new_table(pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}))
content.new_inline_asset(id="438e454b-cf9e-40d9-b53d-b9b250087179", asset_type="service", view_mode="card")
content.new_inline_asset(id="8891046c-b52c-432f-b9b0-ca9515bb1c20", asset_type="file", view_mode="preview")
```

Valid methods of the `Editor` class are:

- `new_header`: adds a header block of specified level (1-3)
- `new_paragraph`: adds a paragraph of text
- `new_code_block`: adds a code block
- `new_table`: adds a table from a pandas DataFrame
- `new_inline_asset`: adds an Ouro asset with an optional visualization of the data (view_mode="preview")

Using the editor only creates a local object with your content.
You can view your content as JSON and Markdown text using the `to_dict` method.

### Create a post

Before you can create a post, you need to have the content you want to post.
The `Editor` class explained above is one way. You can also create an `Editor` directly from a markdown string:

```python showLineNumbers
markdown_string = """
# Hello World
This is a test post from the Python SDK
"""

content = ouro.posts.Editor(text=markdown_string)
```

When the `Editor` is created via `ouro.posts.Editor()`, it's automatically connected to the Ouro client. Passing `text` to the constructor triggers server-side markdown parsing, which handles standard markdown as well as Ouro-specific syntax like @mentions and asset embeds.

You can also call `from_markdown` explicitly on an existing editor:

```python showLineNumbers
content = ouro.posts.Editor()
content.new_header(level=1, text="Hello World")
content.from_markdown("More content parsed from markdown")
```

This is especially useful for converting LLM output. You can ask the model to
return Ouro extended markdown with a prompt like this:

<Callout type="info">
  Write responses in extended markdown. Mention users as `@username`, prefer
  typed asset links like `[results](dataset:&lt;uuid&gt;)`, and use
  `assetComponent` blocks when the reader needs a rich preview.
</Callout>

````markdown
```assetComponent
{
  "id": "<uuid>",
  "assetType": "dataset",
  "viewMode": "preview",
  "displayConfig": {
    "visualizationId": "<dataset-view-uuid-or-null>",
    "actionId": "<route-action-uuid-or-null>"
  }
}
```
````

When you embed an asset, you don't need to provide the title, description, or
link; Ouro renders those from the asset. Set `assetType` to `post`, `file`,
`dataset`, `route`, or `service`. For files and datasets, prefer `preview`;
otherwise use `card`. For datasets, set
`displayConfig.visualizationId` to render a saved dataset view. For routes, set
`displayConfig.actionId` to pin a specific run.

Once you have your content, you save it to Ouro with the `ouro.posts.create` method.

```python showLineNumbers
post = ouro.posts.create(
    content=content,
    name="Hello World",
    description="This is a post from the Python SDK",
    visibility="public",
)
# Returns a Post object
print(post)
```

If you don't need block-by-block control, you can skip the `Editor` entirely and pass markdown or a file path directly:

```python showLineNumbers
post = ouro.posts.create(
    name="Hello World",
    content_markdown="# Hello World\nThis is a test post from the Python SDK",
    visibility="public",
)

# Or from a markdown file
post = ouro.posts.create(
    name="Hello World",
    content_path="./my-post.md",
    visibility="public",
)
```

Provide exactly one of `content`, `content_markdown`, or `content_path`.

Note: If you start your content with an H1, the name of the post should match the H1.

### Read a post

You can read a post with the `ouro.posts.retrieve` method.
You can find the ID of a post in the response of the `ouro.posts.create` method or from the web interface.

```python showLineNumbers
post_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
post = ouro.posts.retrieve(post_id)

# Returns a Post object
print(post)
```

You will find the post's content in the `content` property of the post object.
You can get a markdown representation of the content with the `text` property.

```python showLineNumbers
post_markdown = post.content.text
print(post_markdown)
```

### Update a post

You can update a post with the `ouro.posts.update` method.

```python showLineNumbers
updated_post = ouro.posts.update(
    post.id,
    description="This is a post from the Python SDK that is now private",
    visibility="private"
)

# Returns the updated post object
print(updated_post)
```

You can update any of the post's properties, including the content, using the same approach used to create it.

### Delete a post

You can delete a post with the `ouro.posts.delete` method. You must be an admin of the post to delete it.

```python showLineNumbers
ouro.posts.delete(post.id)
```

## Quests

Quests are team-scoped requests for help. Use `ouro.quests` to create quests,
manage items, submit entries, and review contributions. See
[Quests on Ouro](/docs/concepts/quests) for the product model.

### Create a quest

```python showLineNumbers
quest = ouro.quests.create(
    name="CeO2 reference patterns",
    description="Collect reference XRD patterns for CeO2 nanoparticles.",
    visibility="organization",
    org_id=org_id,
    team_id=team_id,
    type="closable",
    status="open",
    items=[
        "Upload a clean .xy or .xye pattern",
        {"description": "Write a short methods note", "expected_asset_type": "post"},
    ],
)
```

### Retrieve and update a quest

```python showLineNumbers
quest = ouro.quests.retrieve(quest_id)
ouro.quests.update(quest_id, status="closed")
```

### Quest items and entries

```python showLineNumbers
items = ouro.quests.list_items(quest_id)
ouro.quests.create_items(quest_id, ["Additional benchmark file"])

entry = ouro.quests.create_entry(
    quest_id,
    item_id=items[0].id,
    asset_id=dataset.id,
    asset_type="dataset",
)
entries = ouro.quests.list_entries(quest_id, status="submitted")
ouro.quests.review_entry(quest_id, entry.id, status="accepted")
```

### Entry submission limits (`type`)

Set `type` when creating a quest (`"closable"` default, or `"continuous"`):

| `type`       | `create_entry` behavior                                                                                                                                                          |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `closable`   | At most one **active** entry per `(item_id, your user)` while status is `submitted` or `accepted`. A second call raises an API error. After **rejection**, you may submit again. |
| `continuous` | No per-user cap — each `create_entry` inserts a new row for the same item.                                                                                                       |

```python showLineNumbers
# Ongoing benchmark — contributors can submit every week
ouro.quests.create(
    name="Weekly structure upload",
    type="continuous",
    items=["Upload this week's relaxed structure"],
    org_id=org_id,
    team_id=team_id,
)

# One-shot bounty — one pending/accepted slot per contributor per item
ouro.quests.create(
    name="Reference XRD pattern",
    type="closable",
    items=["Upload a clean .xy pattern"],
    org_id=org_id,
    team_id=team_id,
)
```

Inspect `quest.quest.type` (nested on the retrieved asset) before retrying failed
submits. Use `assets={key: asset_id}` for multi-input items; see
[Quests on Ouro](/docs/concepts/quests).

### Delete a quest

```python showLineNumbers
ouro.quests.delete(quest_id)
```

## Conversations

Conversations let users exchange messages. You can create, list, retrieve,
update, and delete conversations, and create or list messages within a thread.

### Create a conversation

Start a conversation by passing member user IDs. Include yourself if you want
the thread to appear in your conversation list.

```python showLineNumbers
conversation = ouro.conversations.create(
    member_user_ids=[my_user_id, teammate_user_id],
    name="Project Alpha",
    org_id=org_id,
    team_id=team_id,
)
```

### List conversations

Start by listing your conversations to find threads you want to work with.

```python showLineNumbers
conversations = ouro.conversations.list()
for c in conversations:
    print(c.id, c.name, c.metadata)
```

### Retrieve a conversation

Once you have an ID, load the conversation to inspect metadata and access
message helpers.

```python showLineNumbers
conversation_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
conversation = ouro.conversations.retrieve(conversation_id)
print(conversation.name, conversation.metadata)
```

### Update a conversation

You can update top-level fields like `name` and `summary`.

```python showLineNumbers
updated = ouro.conversations.update(
    conversation_id,
    name="Project Alpha",
    summary="Research thread"
)
print(updated)
```

### Create a message

Send a message to a conversation as plain text or structured JSON. Use the
`conversation.messages.create` helper.

```python showLineNumbers
# Text message
msg = conversation.messages.create(text="Hello team!")

# JSON message (rich content)
editor = ouro.posts.Editor()
editor.new_paragraph(text="Hello team!")
msg2 = conversation.messages.create(json=editor.to_dict())
```

### List messages

Read the latest messages to understand the current context of the thread.

```python showLineNumbers
messages = conversation.messages.list()
for m in messages:
    print(m.get("id"), m.get("text") or m.get("json"))
```

### Delete or leave a conversation

`ouro.conversations.delete` removes the conversation when you are the only
member. Otherwise it removes you from the member list (leave the thread).

```python showLineNumbers
ouro.conversations.delete(conversation_id)
```

## Comments

Comments are lightweight, rich-text notes attached to any asset (files, datasets, posts, routes, services, conversations). One-level threads are supported: top-level comments on an asset, and replies to those comments. Use the built-in `Editor` to compose content, then create, list, and update comments.

### Create a comment

Compose with the `Editor`, then create a comment on a parent asset by ID.

```python showLineNumbers
parent_asset_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"  # can be any asset id

editor = ouro.comments.Editor()
editor.new_paragraph(text="Great post! I especially liked the dataset example.")

comment = ouro.comments.create(
    parent_id=parent_asset_id,
    content=editor,
    visibility="public",
)
print(comment.id)
```

### List comments for an asset

Fetch all top-level comments attached to an asset.

```python showLineNumbers
comments = ouro.comments.list_by_parent(parent_asset_id)
for c in comments:
    # The content is stored as text + JSON structure
    print(c.id, c.content.text)
```

### Retrieve a comment

```python showLineNumbers
fetched = ouro.comments.retrieve(comment.id)
print(fetched.content.text)
```

### Update a comment

Rebuild the content with `Editor` or pass an updated `Content` object.

```python showLineNumbers
update_editor = ouro.comments.Editor()
update_editor.new_paragraph(text="Edited: adding one more note.")

updated_comment = ouro.comments.update(
    comment.id,
    content=update_editor,
    visibility="private",
)
print(updated_comment)
```

### Create and list replies

Replies are simply comments whose parent is a comment. Only one level of replies is supported.

```python showLineNumbers
# Create a reply to a top-level comment
reply_editor = ouro.comments.Editor()
reply_editor.new_paragraph(text="Replying here with more details.")

reply = ouro.comments.create(
    parent_id=comment.id,  # parent is the comment id
    content=reply_editor,
    visibility="public",
)

# List replies for a top-level comment
replies = ouro.comments.list_replies(comment.id)
for r in replies:
    print(r.id, r.content.text)
```
