API reference¶
Generated from the source docstrings. Everything listed here is importable from the top-level
omniframes package (functions is conventionally imported 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.
decomposition_row_cap
property
¶
The opt-in cap on a mixed aggregation's raw scan; None means unlimited.
whoami
property
¶
The cached whoami payload, running the preflight on first access.
envelope_options
¶
The session-level knobs the compiler puts on the run envelope.
verify
¶
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
¶
Execute a run envelope, after the (cached) preflight.
plan
¶
Run an envelope with planOnly: true — schema and SQL, no data.
document_queries
¶
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 ( |
None
|
Raises:
| Type | Description |
|---|---|
OmniframesError
|
Omni generated no query for this prompt (400), or the org's AI credits are exhausted (402). |
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
¶
(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 |
DataFrame¶
DataFrame
¶
DataFrame(
session: OmniSession,
plan: PlanNode,
*,
totals: bool = False,
)
A lazy table of rows produced by an Omni query.
write
property
¶
write: DataFrameWriter
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
¶
The output column names, aliases applied. Compiles the plan; performs no I/O.
select
¶
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
¶
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
¶
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
¶
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 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
¶
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.
to_polars
¶
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
¶
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
¶
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
¶
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
¶
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
¶
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
¶
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.
agg
¶
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.
Column¶
Column
¶
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.
descending
property
¶
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.
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'sBETWEENkind, 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
¶
A reference to a model field, by fully qualified name ("users.state").
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
¶
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.
min
¶
Ad-hoc MIN over a raw column (shadows the builtin — see :func:sum).
max
¶
Ad-hoc MAX over a raw column (shadows the builtin — see :func:sum).
count_distinct
¶
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¶
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
¶
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
¶
FeatureFlagError
¶
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
¶
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
¶
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.