Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Collapse generated code in GitHub diffs and exclude it from language stats.
# Reviewers should review spec/openapi.json changes and hand-written files;
# CI's drift check guarantees the generated code matches the spec.
omni_python_sdk/** linguist-generated=true
omni_python_sdk/helpers.py linguist-generated=false
spec/openapi.json linguist-generated=true
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: pip install -e '.[dev]'
- name: Run tests
run: pytest

generated-code-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install generator
run: pip install 'openapi-python-client>=0.29.0,<0.30.0'
- name: Regenerate from checked-in spec
run: scripts/generate.sh
- name: Fail on drift
run: |
git diff --exit-code -- omni_python_sdk spec || {
echo '::error::Generated code is out of sync with spec/openapi.json. Run scripts/generate.sh and commit the result.'
exit 1
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ __pycache__/
.DS_Store
build/
dist/
omni_python_sdk.egg-info/
omni_python_sdk.egg-info/
.venv-test/
spec/openapi.processed.json
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

## 1.0.0 (unreleased)

Full rewrite: the SDK is now generated from the official Omni OpenAPI spec.

- **Breaking:** the hand-written `OmniAPI` class is removed. Queries move to
`omni_python_sdk.helpers` (`client_from_env`, `run_query_blocking`,
`wait_query_blocking`); all other operations are generated endpoint modules
under `omni_python_sdk.api.<tag>` — see the README migration table.
- **Breaking:** errors now raise (`httpx` exceptions / typed responses)
instead of printing and returning `None`.
- **Breaking:** Python 3.10+ required (was 3.9+).
- Coverage grows from ~30 hand-written endpoints to all 195 operations in the
spec (queries, documents, models, connections, SCIM, schedules, AI, embed,
and more), with typed models and sync + async variants.
- Packaging modernized to `pyproject.toml`; fixes the incorrect `dotenv`
dependency (now `python-dotenv`).
- Spec synced from omni repo commit `c3fe7934808a8086999643252e5c19d0917ed171`
(see `spec/provenance.json`), which fixes the query endpoints' declared
content types to NDJSON with typed stream-line models
(exploreomni/omni#57144) and adds AI credit-control entity groups, model
suggestions, and dashboard-removal endpoints (128 paths / 201 operations).
2 changes: 0 additions & 2 deletions MANIFEST.in

This file was deleted.

148 changes: 109 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,136 @@
# omni-python-sdk

Python SDK for interacting with the Omni API
Python SDK for the [Omni Analytics API](https://docs.omni.co/docs/API/), generated from the official OpenAPI spec. Covers the full public API surface (195 endpoints across queries, documents, models, connections, SCIM user/group management, schedules, AI, and more), with typed request/response models and both sync and async support.

## Installation

```bash
pip install -r requirements.txt
pip install omni-python-sdk
```

## Usage
Requires Python 3.10+.

## Authentication

Create an API key in Omni under **Settings → API Keys**, then either export it:

```bash
export OMNI_API_KEY="your-api-key"
export OMNI_BASE_URL="https://myorg.omniapp.co"
```

(or put the same two lines in a `.env` file) and build a client:

```python
from omni_python_sdk import OmniAPI
from omni_python_sdk.helpers import client_from_env

client = client_from_env()
```

# Set your API key and base URL
api_key = "your_api_key"
base_url = "https://your_domain.omniapp.co"
#these can optionally be set in an .env file with the following keys:
# OMNI_API_KEY=<<your api key>>
# OMNI_BASE_URL=<<your base url>>
Or construct one explicitly:

```python
from omni_python_sdk import AuthenticatedClient

client = AuthenticatedClient(base_url="https://myorg.omniapp.co", token="your-api-key")
```

## Running queries

The query endpoints return Apache Arrow data. The `helpers` module handles polling and decoding for you:

```python
from omni_python_sdk.helpers import client_from_env, run_query_blocking

client = client_from_env()

# Define your query
query = {
"query": {
"sorts": [
{
"column_name": "order_items.created_at[date]",
"sort_descending": False
}
],
"limit": 100,
"sorts": [{"column_name": "order_items.created_at[date]"}],
"table": "order_items",
"fields": [
"order_items.created_at[date]",
"order_items.sale_price_sum"
],
"modelId": "your_model_id",
"join_paths_from_topic_name": "order_items"
"fields": ["order_items.created_at[date]", "order_items.sale_price_sum"],
"modelId": "your-model-id",
}
}

# Initialize the API with your credentials
api = OmniAPI(api_key, base_url)
# if you've optionally set your keys in a .env file no arguments are required:
# api = OmniAPI()
# if your environment variables are stored in an alternative location
# api = OmniAPI(env_file='<<path_to_custom_env>>')
table, fields = run_query_blocking(client, query) # table is a pyarrow.Table
df = table.to_pandas()
```

# Run the query and get a table
table = api.run_query_blocking(query)
Tip: copy a ready-made query body from any workbook via **View → Query Structure**.

# Convert the table to a Pandas DataFrame
df = table.to_pandas()
> **Note:** always use these helpers for queries — don't call the generated
> `omni_python_sdk.api.query.query_run` / `query_wait` modules directly. The
> endpoints stream NDJSON (multiple lines per response) with base64-encoded
> Arrow IPC data; the spec now models the line schemas (`QueryStreamJobLine`
> etc.), but the generated response parsing can't consume a multi-line stream
> at runtime. The helpers own that decoding (and job polling, and surfacing
> query errors).

## Calling any endpoint

Every API operation is a module under `omni_python_sdk.api.<tag>`, with four variants: `sync`, `sync_detailed`, `asyncio`, and `asyncio_detailed`.

# Display the first few rows of the DataFrame
print(df.head())
```python
from omni_python_sdk.api.whoami import whoami
from omni_python_sdk.api.scim import scim_users_list
from omni_python_sdk.api.documents import documents_create

me = whoami.sync(client=client)

users = scim_users_list.sync(client=client, count="50")

response = documents_create.sync_detailed(client=client, body=...)
print(response.status_code, response.parsed)
```

Request/response models live in `omni_python_sdk.models` and convert to/from plain dicts with `.to_dict()` / `.from_dict()`.

Async is the same modules:

```python
result = await whoami.asyncio(client=client)
```

See [`examples/`](examples/) for end-to-end scripts (queries, user management, document migration, embed sessions, semantic-view generation).

## Migrating from 0.x

Version 1.0 is a full rewrite: the hand-written `OmniAPI` class is gone, replaced by the generated client above. The most common patterns map as follows:

| 0.x | 1.x |
|---|---|
| `OmniAPI()` | `client_from_env()` from `omni_python_sdk.helpers` |
| `api.run_query_blocking(body)` | `run_query_blocking(client, body)` from `omni_python_sdk.helpers` |
| `api.create_user(body)` etc. | `omni_python_sdk.api.scim.scim_users_create.sync(client=client, body=...)` etc. |
| `api.document_export(id)` | `omni_python_sdk.api.unstable.unstable_documents_export.sync(client=client, identifier=id)` |

## Regenerating the SDK

The client is generated from the vendored spec in `spec/openapi.json` using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) (the generator needs Python 3.11+, though the SDK itself runs on 3.10):

```bash
pip install openapi-python-client

scripts/generate.sh # regenerate from the checked-in spec
scripts/generate.sh --url https://myorg.omniapp.co # sync the spec from a live instance first
scripts/generate.sh --source ../omni/packages/bi-app/app/types/api/openapi/openapi.json
```

To run the example, you need to replace `your_api_key`, `your_domain`, and `your_model_id` with your own values.
The pipeline preprocesses the spec (`scripts/preprocess_spec.py`), regenerates `omni_python_sdk/` (preserving the hand-written `helpers.py`), and CI fails if the checked-in generated code drifts from the checked-in spec. When the spec is synced (`--source`/`--url`), `spec/provenance.json` records where it came from — including the omni repo commit SHA — so every SDK version is traceable to an exact API state.

## Generated code policy

Everything in `omni_python_sdk/` **except `helpers.py`** is generated — don't edit it by hand; changes belong in the spec (upstream in the omni repo) or in the generation pipeline. Generated files are marked `linguist-generated` in `.gitattributes`, so GitHub collapses them in PR diffs.

**Reviewing a spec-sync PR:** review the `spec/openapi.json` diff and any hand-written changes; skip the generated diff. That's safe because CI's drift check proves the generated code is a pure function of the checked-in spec.

To get a query object, you can use the Inspector on a Omni Workbook. The query object is a JSON object that represents the query you want to run. You can find the Inspector in the View menu on a Workbook. Look for the "Query Structure" section.
**Versioning:** the SDK follows its own semver, independent of the API's `info.version` — major for breaking surface changes, minor for new endpoints/fields (most spec syncs), patch for regeneration fixes. See [VERSIONING.md](VERSIONING.md) for how to classify a spec sync (including mechanical breaking-change detection with oasdiff), how to handle generator upgrades, and the release process. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).

For a simple command line interface, you can run the following command:
## Development

```bash
python3 examples/query.py OMNI_API_KEY https://OMNI_URL '{"query": {"sorts": [{"column_name": "omni_dbt__order_items.created_at[date]", "sort_descending": false}], "table": "omni_dbt__order_items", "fields": ["omni_dbt__order_items.created_at[date]", "omni_dbt__order_items.total_sale_price"], "modelId": "OMNI_MODEL_ID", "join_paths_from_topic_name": "order_items"}}
pip install -e '.[dev]'
pytest
```
64 changes: 64 additions & 0 deletions VERSIONING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Versioning

This SDK follows [semantic versioning](https://semver.org/). The SDK version is
**independent of the API's `info.version`** in the OpenAPI spec and of Omni app
releases — it describes the SDK's own surface: the generated client, the
hand-written `helpers.py`, and the package's runtime requirements.

The version lives in one place: `pyproject.toml`.

## What each bump means

| Bump | When | Examples |
|---|---|---|
| **Major** | A change a working program could break on | Endpoint or model removed/renamed; parameter or field type changed; required parameter added; `helpers.py` signature changed; Python version floor raised; generator upgrade that reshapes generated signatures |
| **Minor** | Purely additive surface | New endpoints or tags; new models; new optional fields or parameters; new helper functions |
| **Patch** | No surface change | Regeneration fixes; docs; dependency pin adjustments; internal generation-pipeline changes |

Most spec syncs are **minor**. The Omni API's own CI runs breaking-change
detection (oasdiff) before spec changes merge, so removals should be rare —
but the SDK sync is where they become a package consumer's problem, so
classify each sync explicitly.

## Classifying a spec sync

1. Sync the spec: `scripts/generate.sh --source ../omni/.../openapi.json`
2. Look at the diff summary: `git diff --stat spec/openapi.json` and the
generated diff (`git diff --stat omni_python_sdk/`). Deleted or renamed
modules under `omni_python_sdk/api/` or `omni_python_sdk/models/` are a
strong breaking signal; only-added files suggest minor.
3. For a mechanical verdict, run [oasdiff](https://github.com/oasdiff/oasdiff)
against the previous spec:

```bash
git show HEAD:spec/openapi.json > /tmp/openapi.old.json
oasdiff breaking /tmp/openapi.old.json spec/openapi.json
```

Any reported breaking change → major bump (or push back on the API change
upstream before shipping it).

## Generator upgrades

The `openapi-python-client` version is pinned in `pyproject.toml` (dev extras).
Upgrading it can rewrite every generated file with **no API change** — and can
also change generated method signatures, which is breaking for SDK users even
though the API didn't move.

- Land generator upgrades as their own PR, clearly labeled, never mixed with a
spec sync.
- Diff the generated output before/after: if signatures or model shapes
changed, it's a major bump; if only formatting/internals changed, patch.

## Release process

1. Decide the bump (above) and update `version` in `pyproject.toml`.
2. Add a `CHANGELOG.md` entry: what changed in API surface terms (endpoints
added/removed, helpers changed), and the omni commit from
`spec/provenance.json` the spec was synced from.
3. Merge to `main`, then create a GitHub release tagged `vX.Y.Z`.
4. The `python-publish.yml` workflow builds and publishes to PyPI via trusted
publishing on release publish — no manual upload.

Every release is traceable: the git tag pins the spec (`spec/openapi.json`)
and `spec/provenance.json` pins the omni repo commit that spec came from.
16 changes: 10 additions & 6 deletions examples/content_migration.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from omni_python_sdk import OmniAPI
from omni_python_sdk import AuthenticatedClient
from omni_python_sdk.api.unstable import unstable_documents_export, unstable_documents_import
from omni_python_sdk.models import DocumentImportBody

api_key = '<<your api key>>'
base_url = '<<your omni host>>'

# Initialize the API with your credentials
api = OmniAPI(api_key, base_url)
# Initialize the client with your credentials
client = AuthenticatedClient(base_url=base_url, token=api_key)

# retrieve the dashboard
dashboard_export = api.document_export('<<dashboard identifier>>')
dashboard_export = unstable_documents_export.sync(
'<<dashboard identifier>>', client=client
).to_dict()
# change the dashboard model id
dashboard_export.update({'baseModelId':'<< model id of new location >>'})
dashboard_export.update({'baseModelId': '<< model id of new location >>'})
# import the modified document
api.document_import(dashboard_export)
unstable_documents_import.sync(client=client, body=DocumentImportBody.from_dict(dashboard_export))
14 changes: 8 additions & 6 deletions examples/databricks_metric_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
from typing import Optional
import yaml
import sys
from uuid import UUID
from examples.topic import Topic
from omni_python_sdk import OmniAPI
from omni_python_sdk.api.models import models_get_topic
from omni_python_sdk.helpers import client_from_env


# Example of using the OmniAPI to get a topic definition and convert to a Snowflake semantic view
# This example assumes you have a valid API key and base URL for the OmniAPI defined in your .env file
# Example of using the Omni API to get a topic definition and convert to a Databricks metric view
# This example assumes you have OMNI_API_KEY and OMNI_BASE_URL defined in your .env file

# SQL Reference
# CREATE VIEW
Expand Down Expand Up @@ -264,10 +266,10 @@ def replace_field(match):
return metric_view

def main(model_id: str, topic_name: str, default_catalog: Optional[str], default_schema: Optional[str]):
client = OmniAPI()
client = client_from_env()

response = client.get_topic(model_id=model_id, topic_name=topic_name)
topic = Topic.model_validate(response)
response = models_get_topic.sync(UUID(model_id), topic_name, client=client)
topic = Topic.model_validate(response.topic.to_dict())
metric_view = metric_view_from_topic(topic, default_catalog, default_schema)
print(metric_view.generate_sql())

Expand Down
Loading
Loading