Skip to main content

incorta-sdk

PyPI version license

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.

  • schemaType fails silently. ?schemaType=TYPO returns 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 carries viewsDetails. 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 N tables.get calls.
  • aggregate defaults to true when omitted. A flat extract written without it returns zero rows with HTTP 200, reporting string columns as double. Both clients always send the flag explicitly.
  • Aggregate queries ignore the top-level sorting list. 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)

Source distribution for incorta-sdk 0.6.3
File Size Uploaded Provenance
incorta_sdk-0.6.3.tar.gz 43.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for incorta-sdk 0.6.3
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page