Skip to content

API reference

Generated from the source docstrings. Everything listed here is importable from the top-level omniframes package (functions is conventionally imported as F).

import omniframes as of
from omniframes import functions as F

Private members (_name) are omitted; they are not part of the public surface and may change without notice.


Session

OmniSession

OmniSession(
    *,
    transport: QueryTransport,
    branch: str | None = None,
    timezone: str | None = None,
    cache: CachePolicy | None = None,
    user_id: str | None = None,
    owns_transport: bool = False,
    decomposition_row_cap: int | None = None,
)

A configured connection to one Omni organization.

branch property

branch: str | None

The model branch every query runs against, if any.

user_id property

user_id: str | None

The impersonated membership id, if any.

decomposition_row_cap property

decomposition_row_cap: int | None

The opt-in cap on a mixed aggregation's raw scan; None means unlimited.

catalog property

catalog: Catalog

Discovery: models, topics, views (read-through cached).

read property

session.read.topic(...) / session.read.view(...).

whoami property

whoami: dict[str, Any]

The cached whoami payload, running the preflight on first access.

close

close() -> None

Release the transport, if this session created it.

envelope_options

envelope_options() -> EnvelopeOptions

The session-level knobs the compiler puts on the run envelope.

verify

verify() -> dict[str, Any]

Run the whoami preflight now and return its payload.

Answers three questions in one call: is the key valid, which models can it see, and which permissions does it hold on each (rolesByModel).

run

run(envelope: Mapping[str, Any]) -> QueryResult

Execute a run envelope, after the (cached) preflight.

plan

plan(envelope: Mapping[str, Any]) -> PlanResult

Run an envelope with planOnly: true — schema and SQL, no data.

document_queries

document_queries(
    document_identifier: str,
) -> tuple[dict[str, Any], ...]

The queries stored on a document's dashboard (CONTRACT_NOTES §4).

Two 404s are normal answers rather than outages, and they mean different things: the identifier is unknown, or the document exists but carries no dashboard (and therefore no saved queries at all). Both are reported as such.

ask

ask(
    prompt: str, *, model: str, topic: str | None = None
) -> DataFrame

Ask Omni's AI for a query, and get back a lazy frame that runs it here.

runQuery is always false (CONTRACT_NOTES §4): omniframes takes the generated query object and executes it through its own pipeline, so the result is normalized, limited and explained like every other frame — and the endpoint needs no query-api flag. explain() shows the prompt that produced the query, and the query itself goes on the wire exactly as Omni wrote it.

Parameters:

Name Type Description Default
prompt str

what to ask for, in English.

required
model str

the model (name or id) the question is about.

required
topic str | None

optional topic to steer the generator (currentTopicName).

None

Raises:

Type Description
OmniframesError

Omni generated no query for this prompt (400), or the org's AI credits are exhausted (402).

SessionBuilder

SessionBuilder()

Collects configuration; :meth:get_or_create builds a session without calling Omni.

Explicit settings take precedence over environment variables, then notebook secrets.

base_url

base_url(base_url: str) -> SessionBuilder

The org URL. acme.omniapp.co, https://acme.omniapp.co and …/api/v1 all work.

host

host(host: str) -> SessionBuilder

Alias of :meth:base_url, for when a bare hostname reads better.

base_url_from_env

base_url_from_env(
    variable: str = BASE_URL_ENV,
) -> SessionBuilder

Read only the environment variable (OMNI_BASE_URL by default).

api_key

api_key(api_key: str) -> SessionBuilder

The org API key or personal access token. Stored only inside the transport.

api_key_from_env

api_key_from_env(
    variable: str = API_KEY_ENV,
) -> SessionBuilder

Read only the environment variable (OMNI_API_KEY by default).

secrets

secrets(
    provider: str | None = "auto",
    *,
    scope: str | None = None,
    api_key_name: str = API_KEY_ENV,
    base_url_name: str = BASE_URL_ENV,
) -> SessionBuilder

Configure notebook secret fallback; reads happen only in :meth:get_or_create.

auto (the default) recognizes loaded Colab or an active notebook's dbutils. Select colab, databricks, snowflake (Snowpark secrets), or snowflake-legacy (Streamlit notebook aliases) explicitly to override discovery. None disables secrets. Snowflake providers require explicit selection.

Databricks requires scope when secret lookup is needed. Names identify provider secrets, not environment variables; Snowflake Workspaces uses database/schema/name. Explicit host/API key values and environment variables take precedence. An injected transport bypasses all automatic credential resolution. No provider is imported here.

branch

branch(branch_id: str) -> SessionBuilder

Run every query against a model branch (top-level branchId; must be a UUID).

timezone

timezone(timezone: str) -> SessionBuilder

An IANA timezone for query results (needs org + connection support server-side).

cache

cache(cache: str | CachePolicy) -> SessionBuilder

Cache policy: Standard, SkipRequery, SkipCache…

The values published in the OpenAPI spec (disabled/normal/…) are rejected by the server, so they are rejected here too — with the list of what actually works.

user_id

user_id(membership_id: str) -> SessionBuilder

Impersonate a membership id (not a user id) — CONTRACT_NOTES §1.

decomposition_row_cap

decomposition_row_cap(rows: int | None) -> SessionBuilder

Cap the raw scan a mixed aggregation pulls down (docs/HYBRID.md §2.1).

SQL-expressible ad-hoc aggregations use a warehouse GROUP BY and fetch no raw rows, so this cap does not apply to them (docs/SQLTIER.md §1). It applies when the splitter falls back to local aggregation over raw rows.

When a mixed agg() needs this fallback, the ad-hoc half is computed here over raw rows, and that scan is unlimited by default: a silently capped input to a local aggregation is a wrong answer, not a truncated page. This is the safety valve for when unlimited is not affordable — it sends limit: rows instead and warns loudly (:class:~omniframes.errors.TruncationWarning) whenever the cap is actually hit, so a capped answer can never pass for a complete one. None (the default) means unlimited.

rate_limit_wait

rate_limit_wait(seconds: float) -> SessionBuilder

Wait up to seconds for each rate-limited GET, not for the whole action.

A catalog action can make multiple GET requests, and each receives its own waiting budget.

transport

transport(transport: QueryTransport) -> SessionBuilder

Use an existing transport instead of building an :class:HttpTransport.

This is the seam tests use (HttpTransport over httpx.MockTransport) and the seam an in-product notebook broker will use.

get_or_create

get_or_create() -> OmniSession

Build without calling Omni; missing settings can use the selected secret provider.

DataFrameReader

DataFrameReader(session: OmniSession)

session.read — the two governed ways into a model's data.

topic

topic(model: str, topic: str) -> DataFrame

Read a topic — the governed default path (needs QUERY_TOPICS).

The topic carries the model's join paths, so fields from any joined view can be selected without saying how they join. Both the model and the topic are resolved through the catalog here, so a typo fails immediately with the available names.

view

view(model: str, view: str) -> DataFrame

Read a bare view, outside any topic (needs QUERY_FULL_MODEL).

No topic means no governed join paths: only this view's own fields are selectable. Prefer :meth:topic unless you specifically want the ungoverned shape.

The name is validated against every view in the composed model — including views no topic reaches, which is the point of a bare-view read (docs/DESIGN.md). That check is one request, not one per topic: it does not need the field metadata it would discard.

sql

sql(model: str, sql: str) -> DataFrame

Run your SQL on the model's connection (needs QUERY_SQL).

The statement is sent as userEditedSQL with rewriteSql: false — the marker that makes the server run the text verbatim instead of parsing it as OmniSQL (CONTRACT_NOTES §3.5). Omniframes never sends one without the other; that is enforced when the step is built, not merely intended.

The SQL is opaque: omniframes does not parse it and cannot push anything into it, so every operation written on top runs in the local engine over its result, and explain() says so. Table names are yours to qualify — the SQL runs against the connection's own schema, not against the model's views.

df.schema works through a planOnly round trip; df.columns needs either that or a select(), because until the server plans the statement nobody knows what it returns.

saved_queries

saved_queries(
    document_identifier: str,
) -> tuple[tuple[str, int], ...]

(name, index) for every query stored on a document's dashboard (§4).

The index is what :meth:saved_query takes when two saved queries share a name.

saved_query

saved_query(
    document_identifier: str,
    name_or_index: str | int = 0,
    *,
    model: str | None = None,
) -> DataFrame

Read a query stored on a document's dashboard and run it verbatim (§4).

The stored blob goes back on the wire exactly as it arrived — omniframes did not write it, and re-serializing it would quietly normalize keys it was handed. The only two things touched are the ones the contract says a client must supply: modelId is verified (or injected from model=), and version is filled in when the blob predates it.

Operations written on top of the frame run locally, for the same reason a raw-SQL scan's do: the query is somebody else's, and rewriting it would no longer be that query.

Parameters:

Name Type Description Default
document_identifier str

the document (workbook/dashboard) identifier.

required
name_or_index str | int

the saved query's name, or its position (default: the first).

0
model str | None

name or id of the model to run it against, when the blob does not say.

None

Raises:

Type Description
OmniframesError

the document is unknown, or exists without a dashboard.

CompileError

no saved query matches name_or_index.


DataFrame

DataFrame

DataFrame(
    session: OmniSession,
    plan: PlanNode,
    *,
    totals: bool = False,
)

A lazy table of rows produced by an Omni query.

session property

session: OmniSession

The session this frame runs against.

logical_plan property

logical_plan: PlanNode

The immutable plan this frame wraps.

write property

df.write.csv(path) / df.write.parquet(path) — runs the query, writes the file.

schema property

schema: OmniSchema

The result schema, from a cached planOnly round trip.

summary.fields is the only schema authority (docs/DESIGN.md §3) — never the catalog: it is what the planner will actually return, grains and all. The plan job runs once per DataFrame instance. The one field omniframes adds itself is row_type, which :meth:with_totals derives client-side and the server therefore never describes.

columns property

columns: tuple[str, ...]

The output column names, aliases applied. Compiles the plan; performs no I/O.

select

select(
    *columns: str | Column | Iterable[str | Column],
) -> DataFrame

Choose the columns to return.

In Omni, selecting dimensions together with governed measures is the group-by, so select("users.state", F.measure("order_items.total_sale_price")) returns one row per state. Strings are field names; :meth:~omniframes.column.Column.alias renames client-side.

group_by

group_by(
    *columns: str | Column | Iterable[str | Column],
) -> GroupedData

Group by dimensions (and grains), then call :meth:GroupedData.agg.

Sugar, not a different query: df.group_by("users.state").agg(F.measure("m")) compiles to exactly the same envelope as df.select("users.state", F.measure("m")), because in Omni the selection is the group-by (docs/DESIGN.md §2). Use whichever reads better.

group_by() with no keys aggregates the whole frame into a single row.

filter

filter(condition: str | Column | Expr) -> DataFrame

Keep the rows matching condition.

Use &, | and ~ — never and/or/not, which Python cannot overload (:class:~omniframes.column.Column raises a TypeError explaining this). A bare column name filters a boolean field.

A predicate on a governed measure (F.measure("order_items.count") > 10) is a genuine HAVING: the server applies it after the group-by, whether the measure is selected or not, and whether the filter is written before or after the group_by() (CONTRACT_NOTES §3.1).

sort

sort(
    *columns: str | Column | Iterable[str | Column],
) -> DataFrame

Order rows. F.col("x").desc() sorts descending; a later sort() replaces it.

limit

limit(n: int | None) -> DataFrame

Cap the number of rows. limit(None) asks for everything (wire limit: null).

Limits compose to the tightest of the two: df.limit(50).limit(10) returns 10 rows.

offset

offset(n: int) -> DataFrame

Skip n rows.

limit and offset travel together as the wire's LIMIT n OFFSET k pair, so df.limit(10).offset(5) returns rows 6..15 of the sorted result — SQL semantics. Repeated calls accumulate.

with_column

with_column(
    name: str, column: str | Column | Expr
) -> DataFrame

Add (or replace) a derived column.

SQL-expressible expressions, such as arithmetic over selected fields, push down to an OmniSQL job. A :func:~omniframes.functions.udf or an expression that cannot run remotely is evaluated locally over the largest remote sub-plan. explain() shows which tier runs the expression and where any local work begins.

map_pandas

map_pandas(
    fn: Callable[[DataFrame], DataFrame],
    schema_hint: OmniSchema | None = None,
) -> DataFrame

Hand the materialized frame to a Python function and take a frame back.

This is the escape hatch: whatever fn does, it runs here, over the result of the largest query omniframes could push down.

A Python function's output schema cannot be planned, so without schema_hint the frame no longer knows its own columns: :attr:schema and :attr:columns raise, and the only operations allowed above it are the ones that need no schema (limit, offset, another map_pandas). Pass schema_hint=OmniSchema(...) to declare what comes back and everything works normally again.

join

join(
    other: DataFrame,
    on: str | Sequence[str],
    how: str = "inner",
) -> DataFrame

Join this frame to other on shared column names, with SQL semantics.

on names columns both frames output — so a field aliased in select() is named by its alias, and an un-aliased one by its wire name. how is inner (default), left, right or outer (full / full_outer mean the same).

NULL keys never match — not even another NULL, exactly as in SQL. A row whose key is NULL takes no part in the matching and reappears only as an unmatched row: dropped by an inner join, kept with the other side's columns NULL by the join type that preserves its side. (This is deliberately not the alignment a decomposed aggregate uses internally, where the two NULL groups are the same group — docs/HYBRID.md §3.2.)

Each side compiles on its own, so joining across two models, or a governed topic to a raw-SQL job, needs nothing special; the join itself always runs locally, because the query API takes one query. explain() shows both sub-plans and the join between them.

Non-key columns that would collide are a :class:~omniframes.errors.CompileError rather than a silently suffixed pair: alias or drop one of them first.

union

union(other: DataFrame) -> DataFrame

Stack other's rows under this frame's (UNION ALL — nothing is de-duplicated).

By position, like PySpark — but omniframes additionally insists the column names line up, because its columns are named wire outputs: quietly relabelling the right side's data with the left side's names is the one outcome nobody wants. Types widen where SQL widens (int64 + float64 → float64); types with no common type are a :class:~omniframes.errors.CompileError.

with_totals

with_totals() -> DataFrame

Ask Omni for the grand-total row and keep it in the result.

The query gains column_totals: {"::total::": {"type": "aggregation"}} and the materialized frame gains a trailing row_type column: "data" on the rows the query grouped, "total" on the appended totals row. The totals row re-aggregates the measures over every row the query touched — post-filter, pre-limit — so it is not the sum of the values above it, which is exactly the point of asking the server for it.

Only meaningful when the query has a measure: without one there is nothing to total, and compiling raises :class:~omniframes.errors.CompileError. The marker rides along through further transformations, so df.with_totals().sort(...) still totals.

collect

collect() -> Table

Run the query and return the normalized result as an Arrow table.

to_arrow

to_arrow() -> Table

Run the query and return an Arrow table.

to_pandas

to_pandas() -> DataFrame

Run the query and return a pandas DataFrame.

to_polars

to_polars() -> Any

Run the query and return a polars DataFrame.

polars is an optional dependency — pip install 'omniframes[polars]' — because most users already have pandas and nobody should pay for a second dataframe library they did not ask for. The conversion is zero-copy through Arrow either way.

omni_url

omni_url() -> str

Run this query and return the Omni workbook URL that opens it in the browser.

The handoff from a notebook to the UI: the same governed query, explorable by whoever you send the link to. Two constraints come straight from the wire (CONTRACT_NOTES §2.1): workbookUrl is rejected alongside staticQueryReferences, and a plan that is a DAG has no single query for a workbook to be of. Both are refused here, before any request, naming which one bit.

Note this executes the query: the URL is minted by the same run that returns the rows, and the rows are then thrown away.

count

count() -> int

The number of rows in the materialized frame.

PySpark-consistent: this counts what collect() would return — i.e. after the applied limit — and inherits its truncation warning. It is not COUNT(*) over the underlying table; select F.measure(...) for a governed count.

first

first() -> dict[str, Any] | None

The first row as a dict, or None when the result is empty.

No truncation warning: a limit of one row is what was asked for.

show

show(n: int = _SHOW_DEFAULT_ROWS) -> None

Print up to n rows as a text table.

Fetches one row more than it prints, purely to know whether to say "only showing top n rows". That extra row is also why show() never raises :class:~omniframes.errors.TruncationWarning: the footer says the same thing without crying wolf on every preview.

explain

explain(analyze: bool = False) -> str

Describe how this frame will run.

With analyze=True the plan is sent to Omni with planOnly: true and the server's own SQL is appended — blanked out for callers without the VIEW_SQL permission, which the output says explicitly rather than pretending there is no SQL.

GroupedData

GroupedData(frame: DataFrame, keys: tuple[Column, ...])

The result of :meth:DataFrame.group_by — call :meth:agg to get a DataFrame back.

Deliberately tiny: it holds the group keys and nothing else. agg() validates what it is handed at build time (a group key must be a dimension, an aggregate must be a measure or an ad-hoc aggregation) so that a typo fails where it was written rather than at action time.

keys property

keys: tuple[Column, ...]

The group keys, in order.

agg

agg(
    *columns: str | Column | Iterable[str | Column],
) -> DataFrame

Aggregate each group.

Takes governed measures (F.measure("order_items.total_sale_price")) and ad-hoc aggregations (F.count_distinct("users.id")). Governed measures always execute remotely. Ad-hoc and mixed aggregations use an OmniSQL job when expressible; otherwise the splitter uses local aggregation, with a separate remote query for governed measures when needed. explain() shows the resulting plan.

DataFrameWriter

DataFrameWriter(frame: DataFrame)

df.write — runs the frame and writes the result to a file.

csv

csv(
    path: str | Path, *, include_header: bool = True
) -> None

Write the frame as CSV.

CSV has no types: a decimal comes back as text and a NULL is an empty field. Use :meth:parquet whenever the values matter more than the readability.

parquet

parquet(
    path: str | Path, *, compression: Compression = "zstd"
) -> None

Write the frame as Parquet — a lossless round trip of the Arrow table.


Column

Column

Column(
    expr: Expr,
    alias: str | None = None,
    *,
    descending: bool | None = None,
)

A lazy column expression with an optional client-side alias.

Every method returns a new Column; nothing mutates. Aliases never reach the wire (the query API has no aliasing) — they are applied as a rename after the result comes back, and sorts/filters written against an alias are reverse-resolved at compile time.

expr property

expr: Expr

The underlying expression.

alias_name property

alias_name: str | None

The client-side alias, if :meth:alias was called.

descending property

descending: bool | None

Sort direction recorded by :meth:desc/:meth:asc; None when unspecified.

alias

alias(name: str) -> Column

Rename this column client-side. Collisions are a build-time :class:CompileError.

grain

grain(grain: str) -> Column

Select a time grain (created_at[month]). Only valid on a bare field reference.

desc

desc() -> Column

Sort this column descending (used inside sort()).

asc

asc() -> Column

Sort this column ascending (used inside sort()).

to_sort_key

to_sort_key() -> SortKey

This column as a :class:SortKey, defaulting to ascending.

is_null

is_null() -> Column

IS NULL.

is_not_null

is_not_null() -> Column

IS NOT NULL.

isin

isin(*values: Any) -> Column

IN (...). Accepts either varargs or a single iterable.

like

like(
    pattern: str, *, case_insensitive: bool = False
) -> Column

SQL LIKE (wire SQL_LIKE); case_insensitive=True is ILIKE.

between

between(low: Any, high: Any) -> Column

Range predicate. Numbers include both ends; dates do not include the upper one.

  • F.col("order_items.quantity").between(3, 5) → 3 <= quantity <= 5, matching PySpark. It compiles to a composite of >=/<= rather than to the wire's BETWEEN kind, whose upper bound is exclusive (CONTRACT_NOTES §3.1).
  • F.col("order_items.created_at").between(date(2025, 7, 1), date(2026, 7, 1)) → created_at >= 2025-07-01 AND created_at < 2026-07-01, i.e. half-open. The date filter arm has no inclusive upper bound and omniframes will not synthesize one by adding "the smallest unit" to a literal that may be relative ("last quarter") or truncated ("2026-01"). Half-open is also what a month/quarter window wants; write (col >= low) & (col < high) to say it explicitly, and remember that <= on a date is not expressible either (use the next boundary instead).

Which arm applies is decided by the literal type (or by .grain()), exactly as for comparisons: the compiler has no field types.


Functions (F)

The F namespace (docs/INTERNALS.md §1)::

from omniframes import functions as F

df.filter(F.col("users.state") == "California")

String arguments are accepted anywhere a Column is — they are auto-wrapped through :func:col, so df.sort("users.state") and df.sort(F.col("users.state")) are the same thing.

The aggregation helpers build :class:~omniframes.column.AdHocAgg nodes. Those are ad-hoc aggregations (computed over raw columns) and are deliberately distinct from :func:measure, which references a governed model measure: a measure always executes remotely, under its server-side definition, while an ad-hoc aggregation is written as SQL over a governed reference core and executed in the warehouse (tier 2, docs/SQLTIER.md) — or, when that is not expressible, computed here over a raw scan (docs/HYBRID.md §2.1). Mixing the two in one agg() is fine: that is the case the splitter decomposes, and explain() shows which tier took which half.

:func:udf is the escape hatch: any Python function, applied row by row. Nothing about a UDF is expressible on the wire, so everything from it up executes locally — explain() shows exactly where that starts.

col

col(name: str | Column) -> Column

A reference to a model field, by fully qualified name ("users.state").

lit

lit(value: LiteralValue) -> Column

A constant.

measure

measure(name: str) -> Column

A governed model measure (F.measure("order_items.total_sale_price")).

Measure definitions live server-side; omniframes never emulates one locally.

sum

sum(column: str | Column) -> Column

Ad-hoc SUM over a raw column.

Shadows the builtin inside this module on purpose: F.sum is the name PySpark users reach for, and the module is meant to be imported as F, never star-imported.

avg

avg(column: str | Column) -> Column

Ad-hoc AVG over a raw column.

min

min(column: str | Column) -> Column

Ad-hoc MIN over a raw column (shadows the builtin — see :func:sum).

max

max(column: str | Column) -> Column

Ad-hoc MAX over a raw column (shadows the builtin — see :func:sum).

count

count(column: str | Column) -> Column

Ad-hoc COUNT over a raw column.

count_distinct

count_distinct(column: str | Column) -> Column

Ad-hoc COUNT(DISTINCT …); its default column name is count_distinct(<field>).

udf

udf(fn: Callable[..., Any]) -> Callable[..., Column]

Wrap a Python function so it can be applied to columns (docs/HYBRID.md §5)::

upper = F.udf(str.upper)
df.with_column("shout", upper("users.state"))

The function is scalar: it is called once per row with that row's operand values, and its return value becomes the cell. Use :meth:~omniframes.dataframe.DataFrame.map_pandas for a vectorized function over the whole frame.

A UDF is never expressible on the wire, so everything from the UDF up executes locally; the query below it is still pushed down as far as it goes, and explain() shows the split.


Schema types

OmniSchema dataclass

OmniSchema(fields: tuple[OmniField, ...])

An ordered collection of fields.

OmniField dataclass

OmniField(
    name: str,
    data_type: OmniDataType,
    view_name: str | None = None,
    label: str | None = None,
    is_dimension: bool | None = None,
    aggregate_type: str | None = None,
    date_type: str | None = None,
    is_calc: bool = False,
    raw: dict[str, Any] = dict(),
)

One field of a result schema (from summary.fields) or the catalog.

OmniDataType

Bases: Enum

Omni's field data types as reported in summary.fields[*].data_type.


Errors and warnings

Every exception omniframes raises derives from OmniframesError; every warning derives from OmniframesWarning. HTTP details never leak above the transport — the one exception is TransportError.status, which carries the status code as an attribute because two endpoints change their advice based on it.

OmniframesError

Bases: Exception

Base class for all omniframes errors.

TransportError

TransportError(message: str, *, status: int | None = None)

Bases: OmniframesError

A network/protocol-level failure talking to the Omni API.

status carries the HTTP status the failure came from, when there was one — None for a request that never got an answer at all. It is the only HTTP detail that crosses the transport seam, and it does so as an attribute rather than as text: two endpoints change their advice based on the status (CONTRACT_NOTES §4), and recovering it by re-reading the message would make the wording load-bearing.

AuthError

AuthError(message: str, *, status: int | None = None)

Bases: TransportError

The bearer token was rejected (invalid, expired, or malformed header).

FeatureFlagError

FeatureFlagError(
    message: str, *, status: int | None = None
)

Bases: TransportError

The organization does not have the query-api feature flag enabled.

The remedy is administrative: an org admin must enable the Query API for the organization (Omni settings), after which the same key works unchanged.

ModelPermissionError

ModelPermissionError(
    message: str,
    *,
    permission: str | None = None,
    status: int | None = None,
)

Bases: TransportError

The key's user lacks a required model permission (e.g. QUERY_TOPICS, QUERY_FULL_MODEL).

QueryError

QueryError(
    message: str,
    *,
    error_type: str | None = None,
    job_id: str | None = None,
    statement: str | None = None,
)

Bases: OmniframesError

A submitted job reached a terminal error state (in-band job error line).

statement carries the OmniSQL text the server rejected, for the one failure that is omniframes' own fault rather than the user's: a tier-2 statement the model no longer binds (docs/SQLTIER.md §8). It stays None for every other job error, including a raw-SQL job, whose SQL the user wrote and already has.

QueryTimeoutError

QueryTimeoutError(
    message: str, *, remaining_job_ids: tuple[str, ...] = ()
)

Bases: OmniframesError

The client-side deadline elapsed while jobs were still running.

CompileError

Bases: OmniframesError

The logical plan cannot be compiled (bad field/alias/grain, invalid combination).

OmniframesWarning

Bases: UserWarning

Base class for omniframes warnings.

TruncationWarning

Bases: OmniframesWarning

Returned row count equals the applied limit — the result is likely truncated.