incorta-sdk
Read Incorta schemas, tables, views, and columns from Python, as the
signed-in user. Sessions come from
incorta-auth — OAuth 2.0 against the
authorization server built into Incorta — so this client has no identity of its
own and no personal access token to store.
The TypeScript twin is @incorta/sdk; the two speak the
same API, model the same distinctions, and share the same INCORTA_*
configuration.
pip install incorta-sdk
Why it is scoped to a user
Every call carries a user's own Incorta access token, so Incorta filters the results: two people hitting the same endpoint of your app see two different catalogs. That is a property of the design, not a setting — there is no app-level identity to over-share from, and nothing to revoke separately when someone leaves.
It also means the scoped client, not the top-level one, is what you hold:
IncortaClient configuration + OAuth (build once, at startup)
└── for_request() → IncortaUserClient (build per request)
├── schemas
└── tables
Configuration
IncortaClient() takes the same settings as IncortaAuth, each falling back to
its environment variable:
| Setting | Environment variable | Meaning |
|---|---|---|
incorta_url |
INCORTA_URL |
Environment root including any context path (often /incorta), without /api/v2 |
tenant |
INCORTA_TENANT |
Tenant name, e.g. default |
client_id |
INCORTA_CLIENT_ID |
OAuth client id (see incorta-auth register) |
client_secret |
INCORTA_CLIENT_SECRET |
OAuth client secret |
secret |
INCORTA_AUTH_SECRET |
Session-cookie encryption key (≥ 32 chars) |
internal_incorta_url |
INCORTA_INTERNAL_URL |
Optional split-horizon address for server-to-server calls |
Plus two of its own: timeout (seconds, default 30) and max_retries
(default 3, for 429/5xx and network errors only — client errors are never
retried).
When the app already builds an IncortaAuth — the usual case, since it needs
one to serve logins — pass it in rather than letting this package create a
second:
from incorta_auth import IncortaAuth
from incorta_sdk import IncortaClient
auth = IncortaAuth(app_access="catalog")
client = IncortaClient(auth=auth)
Usage
FastAPI
from fastapi import Depends, FastAPI, Request
from incorta_auth import IncortaAuthMiddleware
from incorta_sdk import IncortaClient
app = FastAPI()
app.add_middleware(IncortaAuthMiddleware) # serves /auth/*, requires a session
client = IncortaClient() # once, at startup
@app.get("/api/schemas")
def schemas(request: Request):
incorta = client.for_request(request) # per request, as this user
return [
{"name": schema.name, "description": schema.description}
for schema in incorta.schemas.physical()
]
@app.get("/api/tables/{schema_name}/{table_name}")
def table(schema_name: str, table_name: str, request: Request):
incorta = client.for_request(request)
obj = incorta.tables.get(schema_name, table_name)
return {
"name": obj.qualified_name,
"columns": [{"name": c.name, "type": c.data_type} for c in obj.columns],
}
for_request works with any request object exposing a headers mapping —
Starlette/FastAPI, Django, and Flask all qualify. Given a session you already
hold (the FastAPI get_session dependency, or request.state), use
client.for_session(session); given a raw header, client.for_cookie_header(...).
Streamlit
import streamlit as st
from incorta_auth.streamlit import incorta_auth
from incorta_sdk import IncortaClient
incorta_auth.require_login()
client = st.cache_resource(IncortaClient)()
incorta = client.for_access_token(incorta_auth.access_token())
st.write([schema.name for schema in incorta.schemas.list()])
Surface
IncortaClient
| Member | Purpose |
|---|---|
auth |
The underlying IncortaAuth — mount its middleware for login |
for_request(request) |
Scoped client for the user behind a request |
for_session(session) |
Scoped client from a session you already read |
for_cookie_header(header) |
Scoped client from a raw Cookie header |
for_access_token(token, *, user=None) |
Scoped client from a bare access token |
info |
base_url, tenant, timeout, max_retries — no secrets |
close() |
Releases the connection pool (only if it built the auth instance) |
IncortaUserClient.schemas
| Method | Returns |
|---|---|
list(type=..., limit=0, offset=0, sort_by=...) |
list[SchemaInfo] |
list_page(...) |
Page[SchemaInfo] — adds the server-side total |
iter_all(type=..., page_size=100) |
Iterator, one page fetched at a time |
physical() / business() |
Shorthands for the type filter |
get(name) |
PhysicalSchema or BusinessSchema, with contents |
exists(name) |
bool |
IncortaUserClient.tables
| Method | Returns |
|---|---|
get(schema, name) |
Table or View, columns populated |
list(schema) / names(schema) |
Every object, or just their names |
columns(schema, name) |
list[Column] |
tables_only(schema) / views_only(schema) |
Filtered by kind |
exists(schema, name) |
bool |
Names are matched case-insensitively.
IncortaUserClient.data
Reads rows out of business views. Fields are addressed by their fully qualified
name, SCHEMA.VIEW.COLUMN.
| Method | Returns |
|---|---|
query(measures, *, rows=..., aggregate=False, filters=..., sorting=..., page_size=0, ...) |
QueryResult |
iter_rows(measures, *, page_size=1000, ...) |
Iterator of rows, one page fetched at a time |
csv(measures, ...) |
str — the CSV Incorta rendered |
raw(body) |
The decoded response for a body sent verbatim |
build_body(measures, ...) |
The request body, without sending it |
result = incorta.data.query(
[Measure(field="HR_BS.Employee_BS.SALARY", aggregation="sum", label="payroll")],
rows=["HR_BS.Employee_BS.JOB_TITLE"],
aggregate=True,
filters=[Filter.on("HR_BS.Employee_BS.JOB_TITLE", "IN_LIST", ["Accountant"])],
sorting=[Sort(field="HR_BS.Employee_BS.JOB_TITLE", direction="desc")],
)
result.headers # ["JOB_TITLE", "payroll"]
result.dicts() # [{"JOB_TITLE": "Accountant", "payroll": "39600.0"}]
result.total_rows # rows matching beyond this page
A bare string is shorthand for Measure(field=...) or Dimension(field=...).
Cells always come back as strings — Incorta renders every value as text.
aggregate=False gives a flat extract; aggregate=True folds each measure with
its aggregation and groups by rows and columns.
Models
Frozen dataclasses in snake_case. PhysicalSchema exposes .tables,
BusinessSchema exposes .views, and both expose .objects so type-agnostic
code works against either. Every model keeps the untouched API record in .raw,
so a field this package does not model is still reachable.
Errors
Everything derives from IncortaError:
IncortaError
├── IncortaConfigError a setting is missing or malformed
├── IncortaAuthRequiredError no signed-in user on this request
├── IncortaSessionExpiredError the captured token aged out (raised locally)
├── IncortaConnectionError environment unreachable
│ └── IncortaTimeoutError
├── IncortaAPIError non-2xx, carrying .status_code and .code
│ ├── AuthenticationError 401 — Incorta refused the token
│ ├── PermissionDeniedError 403 — this user lacks access
│ ├── NotFoundError 404
│ │ └── SchemaNotFoundError
│ └── IncortaServerError 5xx
└── TableNotFoundError detected client-side, lists what does exist
Token lifetime
for_request and for_cookie_header refresh the access token as they read the
session, so a client built per request always starts fresh. A scoped client held
past its token's expiry raises IncortaSessionExpiredError before making a
request, rather than letting Incorta answer 401 — build one per request and the
case never arises.
Behaviour both SDKs share
These are the API quirks the SDKs exist to absorb, handled identically in Python and TypeScript.
schemaTypefails silently.?schemaType=TYPOreturns HTTP 200 with business schemas, and so does omitting the parameter. A typo would hand you plausible but wrong data, so both clients validate the value locally and always send it explicitly.- Physical and business schemas return disjoint keys. A physical schema
carries
tablesDetails; a business schema carriesviewsDetails. The other key is absent entirely, not empty. Both clients return a different type for each rather than one half-null shape. - The API misspells its own value as
BUSSINESS_VIEW(three S's). Both clients round-trip that spelling and accept the corrected one, so nothing breaks whichever way Incorta resolves it. - There is no per-table endpoint. Fetching one table means fetching its
whole schema, so prefer
schemas.get(name)once over Ntables.getcalls. aggregatedefaults to true when omitted. A flat extract written without it returns zero rows with HTTP 200, reporting string columns asdouble. Both clients always send the flag explicitly.- Aggregate queries ignore the top-level
sortinglist. Sorting is read only from inside a dimension. Both clients route each sort onto the dimension it names, and reject a sort matching none rather than letting it vanish. format: "csv"cannot be unstringified. Asking for both returns the header line alone, with HTTP 200. Both clients pick the encoding themselves.nullValueAs: "DASH"is documented but rejected with HTTP 400. Both clients omit it from the accepted values and say why.- The query endpoint uses a different error envelope,
{"errorMessages": [{"message": "INC_..."}]}, and answers some 400s in plain text rather than JSON. Both clients parse all three shapes onto the same error object. - Errors carry a stable
INC_code inside{"message": "INC_09030108: ..."}. Both clients parse it onto the error object separately from the prose. - Tokens never appear in logs,
repr(), or a client's public surface.
Development
incorta-auth is resolved from ../auth-python ([tool.uv.sources]), so the
SDK is always checked against the auth code in this commit rather than the last
release:
uv sync
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest
Released off the same py-v{version} tag as incorta-auth, at the same
version; the publish pipeline pins the dependency to that exact version. See
the root README.
Metadata
Release files for incorta-sdk 0.6.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| incorta_sdk-0.6.3.tar.gz | 43.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| incorta_sdk-0.6.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.7 kB
Release files / incorta_sdk-0.6.3.tar.gz
| Download URL | incorta_sdk-0.6.3.tar.gz |
|---|---|
| Size | 43.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
69f3408aaab2e0a3353154cd8bebf9bdfdb0f3732262c993051e31a2f1dc968a
|
|
BLAKE2b-256 checksum How to use checksums |
1d11a9432f33642d4e312e2a765b95635c369aaf358b66b99776f9c52260e6c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / incorta_sdk-0.6.3-py3-none-any.whl
| Download URL | incorta_sdk-0.6.3-py3-none-any.whl |
|---|---|
| Size | 38.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
179e3adca341a7de5a57e675798f56bd9901604d7285e90bd1ec73b068e4500a
|
|
BLAKE2b-256 checksum How to use checksums |
e45f14f0f14ae07b037c699d11b768c49498a6a73f49704c835f780c4091ad64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for incorta-sdk 0.6.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.