Skip to content

Python Client (provisa-client)

Python client for Provisa. Provides four interfaces:

Interface Use case
ProvisaClient GraphQL queries, Arrow Flight, DataFrame output
DB-API 2.0 (connect) Standard Python database interface (PEP 249) (REQ-268)
SQLAlchemy dialect BI tools, ORM, Pandas read_sql (REQ-270)
ADBC Arrow-native columnar streaming via Flight (REQ-271)

Install

pip install provisa-client                      # core (ProvisaClient + DB-API)
pip install "provisa-client[pandas]"            # adds pandas
pip install "provisa-client[sqlalchemy]"        # adds SQLAlchemy dialect
pip install "provisa-client[adbc]"              # adds ADBC over Arrow Flight

ProvisaClient

Quick Start

from provisa_client import ProvisaClient

client = ProvisaClient(
    "http://localhost:8001",
    token="provisa_pat_...",   # personal access token, or a provider bearer token
    role="analyst",
)

ProvisaClient takes a credential, not a username and password: it holds no login step of its own. A personal access token is the credential to reach for when a script needs to run unattended — it is issued from the user's own profile, carries an expiry, and is revocable without touching the account. (REQ-1263) A provider bearer token works identically. Either goes in token, and the client presents it on both the HTTP and the Arrow Flight path.

To exchange a password for a token, POST to /auth/login and read access_token:

import httpx

body = httpx.post(
    "http://localhost:8001/auth/login",
    json={"username": "alice", "password": "secret"},
).json()
client = ProvisaClient("http://localhost:8001", token=body["access_token"])

The DB-API and ADBC entry points do this exchange for you — see below.

GraphQL Queries

# Raw response dict
result = client.query("{ orders { id amount region } }")

# With variables
result = client.query(
    "query Q($region: String!) { orders(region: $region) { id amount } }",
    variables={"region": "west"},
)

# pandas DataFrame (first root field is flattened)
df = client.query_df("{ orders { id amount region } }")

Async

result = await client.aquery("{ orders { id amount } }")

Arrow Flight (high-throughput columnar)

Use Flight for large result sets — data streams as Arrow record batches without materializing on the server. (REQ-143, REQ-145)

import pyarrow as pa

table: pa.Table = client.flight("{ orders { id amount region } }")
df = client.flight_df("{ orders { id amount region } }")

Flight connects to port 8815 by default. (REQ-143) Override with flight_port=:

client = ProvisaClient("http://prod.example.com", flight_port=8815)

Catalog Exploration

tables_df = client.list_tables()

Connection Reference

Parameter Default Description
url http://localhost:8001 Provisa server base URL
token None Bearer credential — a provider token or a personal access token; omit for password auth (REQ-606, REQ-1263)
role None Role to act as, sent as X-Provisa-Role and honored only when assigned to the user; omit to run as the identity's own role (REQ-273)
flight_port 8815 Arrow Flight gRPC port (REQ-143)

Error Handling

query() raises httpx.HTTPStatusError on HTTP errors. (REQ-607)
query_df() raises RuntimeError if the response contains GraphQL errors. (REQ-607)


Using Provisa in Jupyter

Install into the running kernel before importing. The pandas extra covers DataFrame output; add sqlalchemy if you also need pandas.read_sql through the dialect.

%pip install "provisa-client[pandas]"

Credentials

Notebooks persist cell output to the .ipynb file. Type a token into a cell and it will be saved to disk alongside your code. Don't do that.

ProvisaClient reads no environment: pass its credentials in explicitly, from environment variables. (provisa_client.query(), below, reads them for you.) [tool-verified]

import os
from provisa_client import ProvisaClient

client = ProvisaClient(
    os.environ["PROVISA_URL"],           # choose your own variable name
    token=os.environ["PROVISA_TOKEN"],   # personal access token
    role=os.environ.get("PROVISA_ROLE"), # optional; omit to run as your own role
    org=os.environ.get("PROVISA_ORG"),   # required on a multi-tenant deployment; None otherwise
)

Set those variables in your shell before starting Jupyter, or load a .env file with python-dotenv. Either way, keep them out of the notebook.

One call: query()

provisa_client.query() takes a SQL, GraphQL or Cypher statement and returns a pandas DataFrame. The server works out which language it is. Nothing is constructed first: the connection comes from the environment or a profile file. [tool-verified]

import provisa_client

df = provisa_client.query("SELECT region, amount FROM shop.orders")
admin_view = provisa_client.query("SELECT region, amount FROM shop.orders", role="org_admin")

role= acts as that role for one read, sent as X-Provisa-Role. The server honours it only for a role you hold. The read goes over Arrow Flight; set transport to http to send it to /data/query instead. await provisa_client.aquery(...) is the awaitable form. [tool-verified]

The connection is read from the first place that names each setting: [tool-verified]

  1. keywords to provisa_client.sign_in(...), which resolves the connection once for every later read;
  2. environment variables: PROVISA_URL, PROVISA_TOKEN, PROVISA_USERNAME, PROVISA_PASSWORD, PROVISA_ROLE, PROVISA_ORG, PROVISA_FLIGHT_PORT, PROVISA_TRANSPORT;
  3. a section of ~/.provisa/credentials: [default], or the one PROVISA_PROFILE (or sign_in(profile=...)) names.
[default]
url = https://provisa.example.com
token = provisa_pat_...
role = analyst

[staging]
url = https://staging.provisa.example.com
username = ana

A token is a personal access token or a session token. Give a username and password instead and the client exchanges them for a session token at POST /auth/login. That works on a deployment whose sign-in provider takes a password (basic, simple, LDAP). Keep the password in PROVISA_PASSWORD rather than the file. A refused sign-in, a missing URL, or a named profile that isn't in the file raises with the reason. Provisa has no sign-in flow a notebook can drive through a browser, so for single sign-on deployments use a personal access token. [tool-verified]

Cell magics

%pip install "provisa-client[notebook]"
%load_ext provisa_client.magics
%%sql eu --role analyst_eu
SELECT region, customer_email FROM shop.orders

%%sql, %%graphql and %%cypher answer the cell with query(). The DataFrame is the cell's result. A name after the magic also binds it in the notebook (eu above); --role acts as that role for the cell. [tool-verified]

JupySQL (%load_ext sql) defines a %%sql as well. Load both and %%sql belongs to whichever extension loaded last. Provisa's takes --role and reads over Arrow Flight. JupySQL works too, through the SQLAlchemy dialect: %sql provisa+http://user:password@host:port connects, %sql/%%sql run governed statements, and %sqlcmd tables -s <domain> and %sqlcmd columns -t <table> -s <domain> list the catalog. [tool-verified]

In the browser (Pyodide)

Under Pyodide, query() reads from the server that served the page, as the page's signed-in user. The host page keeps that user on the kernel's global scope:

self.provisaAuth = { baseUrl: "", role: "analyst_eu", bearer: "<token>", org: null };

baseUrl is empty for the page's own origin. The client reads provisaAuth on every request, so a rotated token takes effect without a restart. Results arrive as Arrow when pyarrow is loaded; without it they come as JSON. A Cypher statement always comes back as JSON. query() blocks, which a Web Worker (where a notebook kernel runs) can do; on the page's main thread it raises and await provisa_client.aquery(...) is the call to use. [tool-verified]

DataFrames from GraphQL, SQL, and Cypher

query_df() accepts all three query languages and returns a pandas DataFrame. [tool-verified]

# GraphQL — first root field is extracted automatically
df = client.query_df("{ orders { id amount region } }")

# SQL
df = client.query_df("SELECT id, amount, region FROM orders WHERE region = 'west'")

# Cypher
df = client.query_df("MATCH (o:Order) RETURN o.id, o.amount, o.region")

GraphQL responses carry the data under a root field name; query_df() extracts it. SQL and Cypher responses carry explicit column names from the server and are mapped directly. [tool-verified]

Arrow Flight for large pulls

flight_df() streams data as Arrow record batches instead of deserializing JSON. For wide tables or result sets over a few hundred thousand rows, it is faster and uses less memory. [tool-verified]

df = client.flight_df("SELECT * FROM events")

Arrow preserves column types — integers stay integers, timestamps stay timestamps. JSON does not. That alone is reason to prefer Flight when the schema matters.

Flight connects on port 8815 by default. Pass flight_port= to the ProvisaClient constructor if your server uses a different port.

pandas.read_sql through the SQLAlchemy dialect

Install the sqlalchemy extra into the kernel:

%pip install "provisa-client[sqlalchemy]"

Then build an engine with a provisa+http:// URL. Put credentials in environment variables, not in the URL string. [tool-verified]

import os
import pandas as pd
from sqlalchemy import create_engine

host = os.environ["PROVISA_HOST"]       # e.g. "localhost:8001"
user = os.environ["PROVISA_USER"]
pwd  = os.environ["PROVISA_PASSWORD"]

engine = create_engine(f"provisa+http://{user}:{pwd}@{host}")

df = pd.read_sql("SELECT id, amount FROM orders", engine)

The dialect posts the username and password to /auth/login and holds the resulting token for the life of the engine — the credentials are not retransmitted on each query. [tool-verified]


DB-API 2.0

Standard PEP 249 interface. (REQ-268) Works with any tool that accepts a DB-API connection.

from provisa_client import connect

conn = connect(
    "http://localhost:8001",
    username="alice",
    password="secret",
    role="analyst",     # optional; omit to run as the role the login returns
)

connect posts the username and password to /auth/login and holds the access_token it gets back, so the connection carries a real credential rather than a name. role requests a role and the server honours it only if the identity is assigned it (REQ-273); omitted, the connection runs as the role the login resolved.

Executing queries

The cursor accepts either GraphQL or SQL — detected automatically. (REQ-268, REQ-274)

cur = conn.cursor()

# GraphQL
cur.execute("{ orders { id amount region } }")
rows = cur.fetchall()           # list of tuples
one  = cur.fetchone()           # single tuple or None
many = cur.fetchmany(size=50)   # up to N tuples

# SQL (routed through Stage 2 governance)
cur.execute("SELECT id, amount FROM orders WHERE region = 'west'")
rows = cur.fetchall()

Column metadata

cur.execute("{ orders { id amount } }")
print(cur.description)
# [('id', None, ...), ('amount', None, ...)]
print(cur.rowcount)

Named parameters

cur.execute(
    "SELECT * FROM orders WHERE region = :region",
    {"region": "west"},
)

Context managers

with connect("http://localhost:8001", username="alice", password="secret") as conn:
    with conn.cursor() as cur:
        cur.execute("{ orders { id amount } }")
        print(cur.fetchall())

SQLAlchemy Dialect

pip install "provisa-client[sqlalchemy]"

URL scheme: provisa+http:// or provisa+https:// (REQ-270)

from sqlalchemy import create_engine, text

engine = create_engine("provisa+http://alice:secret@localhost:8001")

with engine.connect() as conn:
    result = conn.execute(text("{ orders { id amount region } }"))
    for row in result:
        print(row)

With pandas

import pandas as pd

df = pd.read_sql("{ orders { id amount } }", engine)

URL parameters

Parameter Description Default
role Role to request; validated server-side (REQ-273) the role the login resolves
engine = create_engine(
    "provisa+http://alice:secret@localhost:8001?role=analyst"
)

Schema introspection

The dialect reflects the catalog your role is served, read from GET /data/catalog as the connection's user, role and org. Each domain is a schema, and every table lives in one: get_schema_names() lists the domains, get_table_names(schema="shop") lists that domain's tables by their SQL names, and get_columns("orders", schema="shop") gives each column's type, nullability and description. With no schema named, get_table_names() lists none. An unknown table raises NoSuchTableError; a catalog the server refuses raises. (REQ-363, REQ-270, REQ-2070) [tool-verified]


ADBC

Arrow Database Connectivity backed by Arrow Flight. (REQ-271) Returns pyarrow.Table directly — no JSON deserialization. (REQ-271)

pip install "provisa-client[adbc]"
from provisa_client.adbc import adbc_connect

conn = adbc_connect(
    "http://localhost:8001",
    user="alice",
    password="secret",
    role="analyst",   # optional; server validates the requested role
    port=8815,        # Arrow Flight port (REQ-711)
)

adbc_connect logs in over HTTP first and puts the resulting token in every Flight ticket, so the Flight server authenticates the connection the same way the REST surface does. (REQ-1263) The role argument is a request, validated server-side against the identity's assignments — it never becomes the identity. (REQ-273)

Fetch as Arrow Table

with conn.cursor() as cur:
    cur.execute("{ orders { id amount region } }")
    table = cur.fetch_arrow_table()   # pyarrow.Table
    df = table.to_pandas()

Fetch as tuples

with conn.cursor() as cur:
    cur.execute("{ orders { id amount } }")
    rows = cur.fetchall()    # list of tuples
    one  = cur.fetchone()    # single tuple or None

Column metadata

cur.execute("{ orders { id amount } }")
print(cur.description)
# [('id', None, ...), ('amount', None, ...)]

Context manager

with adbc_connect("http://localhost:8001", user="alice", password="secret") as conn:
    with conn.cursor() as cur:
        cur.execute("{ orders { id amount } }")
        table = cur.fetch_arrow_table()

ADBC connects to the Flight server on port 8815 by default. (REQ-143) Pass port= to reach a Flight server bound to a non-default port. (REQ-711)