Cliente Python (provisa-client)¶
Cliente Python para o Provisa. Fornece quatro interfaces:
| Interface | Caso de uso |
|---|---|
ProvisaClient |
Consultas GraphQL, Arrow Flight, saída DataFrame |
DB-API 2.0 (connect) |
Interface padrão de banco de dados Python (PEP 249) |
| Dialeto SQLAlchemy | Ferramentas de BI, ORM, read_sql do Pandas |
| ADBC | Streaming colunar Arrow-nativo via Flight |
Instalação¶
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¶
Início Rápido¶
from provisa_client import ProvisaClient
client = ProvisaClient(
"http://localhost:8001",
token="provisa_pat_...", # personal access token, or a provider bearer token
role="analyst",
)
ProvisaClient recebe uma credencial. Ele não tem etapa de login própria, então nunca pede usuário e senha. Quando um script precisa rodar sem supervisão, use um token de acesso pessoal: você o emite no seu próprio perfil, ele tem validade e pode ser revogado sem mexer na conta. Um token bearer do provedor funciona igual. Qualquer um dos dois vai em token, e o cliente o apresenta tanto no caminho HTTP quanto no Arrow Flight.
Para trocar uma senha por um token, faça POST em /auth/login e leia 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"])
Os pontos de entrada DB-API e ADBC fazem essa troca por você, como descrito abaixo.
Consultas GraphQL¶
# 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 } }")
Assíncrono¶
Arrow Flight (colunar de alta vazão)¶
Use o Flight para conjuntos de resultados grandes. Os dados fluem como lotes de registros Arrow e nunca são materializados no servidor.
import pyarrow as pa
table: pa.Table = client.flight("{ orders { id amount region } }")
df = client.flight_df("{ orders { id amount region } }")
O Flight se conecta à porta 8815 por padrão. Sobreponha com flight_port=:
Exploração de Catálogo¶
Referência de Conexão¶
| Parâmetro | Padrão | Descrição |
|---|---|---|
url |
http://localhost:8001 |
URL base do servidor Provisa |
token |
None |
Credencial bearer — um token do provedor ou um token de acesso pessoal; omita para autenticação por senha |
role |
None |
Função com a qual agir, enviada como X-Provisa-Role e atendida apenas se atribuída ao usuário; se omitida, a requisição é executada com a função própria da identidade |
flight_port |
8815 |
Porta gRPC do Arrow Flight |
Tratamento de Erros¶
query() lança httpx.HTTPStatusError em erros HTTP.
query_df() lança RuntimeError se a resposta contiver erros GraphQL.
Usar o Provisa no Jupyter¶
Instale no kernel em execução antes de importar. O extra pandas cobre a saída como DataFrame; adicione sqlalchemy se também precisar de pandas.read_sql pelo dialeto.
Credenciais¶
Os notebooks gravam a saída das células no arquivo .ipynb. Um token digitado numa célula é salvo em disco junto com o seu código. Não faça isso.
ProvisaClient não lê o ambiente: passe as credenciais explicitamente, a partir de variáveis de ambiente. (provisa_client.query(), abaixo, as lê por você.)
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
)
Defina essas variáveis no shell antes de iniciar o Jupyter, ou carregue um arquivo .env com python-dotenv. De qualquer forma, mantenha-as fora do notebook.
Uma chamada: query()¶
provisa_client.query() recebe uma instrução SQL, GraphQL ou Cypher e retorna um DataFrame do pandas. O servidor descobre qual linguagem é. Nada é construído antes: a conexão vem do ambiente ou de um arquivo de perfil.
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= atua com esse papel por uma leitura, enviado como X-Provisa-Role. O servidor só o respeita para um papel que você tem. A leitura vai pelo Arrow Flight; defina transport como http para enviá-la a /data/query. await provisa_client.aquery(...) é a forma aguardável.
A conexão é lida do primeiro lugar que nomeia cada configuração:
- palavras-chave de
provisa_client.sign_in(...), que resolve a conexão uma vez para todas as leituras seguintes; - variáveis de ambiente:
PROVISA_URL,PROVISA_TOKEN,PROVISA_USERNAME,PROVISA_PASSWORD,PROVISA_ROLE,PROVISA_ORG,PROVISA_FLIGHT_PORT,PROVISA_TRANSPORT; - uma seção de
~/.provisa/credentials:[default], ou a quePROVISA_PROFILE(ousign_in(profile=...)) nomeia.
[default]
url = https://provisa.example.com
token = provisa_pat_...
role = analyst
[staging]
url = https://staging.provisa.example.com
username = ana
Um token é um token de acesso pessoal ou um token de sessão. Se você informar username e password no lugar, o cliente os troca por um token de sessão em POST /auth/login. Isso funciona num deployment cujo provedor de login aceita senha (basic, simple, LDAP). Guarde a senha em PROVISA_PASSWORD, fora do arquivo. Um login recusado, uma URL ausente ou um perfil nomeado que não está no arquivo gera um erro com o motivo. O Provisa não tem fluxo de login que um notebook consiga conduzir por um navegador, então em deployments com single sign-on use um token de acesso pessoal.
Magics de célula¶
%%sql, %%graphql e %%cypher respondem à célula com query(). O DataFrame é o resultado da célula. Um nome após o magic também o vincula no notebook (eu acima); --role atua com esse papel na célula.
O JupySQL (%load_ext sql) também define um %%sql. Carregue os dois e %%sql pertence à extensão carregada por último. A do Provisa aceita --role e lê pelo Arrow Flight. O JupySQL também funciona, pelo dialeto SQLAlchemy: %sql provisa+http://user:password@host:port conecta, %sql/%%sql executam instruções governadas, e %sqlcmd tables -s <domain> e %sqlcmd columns -t <table> -s <domain> listam o catálogo.
No navegador (Pyodide)¶
No Pyodide, query() lê do servidor que serviu a página, como o usuário conectado na página. A página hospedeira mantém esse usuário no escopo global do kernel:
baseUrl fica vazio para a própria origem da página. O cliente lê provisaAuth a cada requisição, então um token rotacionado vale sem reiniciar. Os resultados chegam como Arrow quando pyarrow está carregado; sem ele, chegam como JSON. Uma instrução Cypher sempre volta como JSON. query() bloqueia, o que um Web Worker (onde roda o kernel de um notebook) pode fazer; na thread principal da página ela gera um erro, e a chamada a usar é await provisa_client.aquery(...).
DataFrames de GraphQL, SQL e Cypher¶
query_df() aceita as três linguagens de consulta e retorna um DataFrame do pandas.
# 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")
As respostas GraphQL trazem os dados sob o nome de um campo raiz; query_df() os extrai. As respostas SQL e Cypher trazem nomes de coluna explícitos do servidor e são mapeadas diretamente.
Arrow Flight para extrações grandes¶
flight_df() transmite os dados como lotes de registros Arrow em vez de desserializar JSON. Para tabelas largas ou resultados acima de algumas centenas de milhares de linhas, é mais rápido e usa menos memória.
O Arrow preserva os tipos de coluna. Inteiros continuam inteiros e timestamps continuam timestamps, o que o JSON não consegue prometer. Quando o esquema importa, isso já basta para preferir o Flight.
O Flight conecta por padrão na porta 8815. Passe flight_port= ao construtor de ProvisaClient se o seu servidor usa outra porta.
pandas.read_sql pelo dialeto SQLAlchemy¶
Instale o extra sqlalchemy no kernel:
Depois monte um engine com uma URL provisa+http://. Leia as credenciais de variáveis de ambiente para que nunca fiquem no seu notebook.
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)
O dialeto envia usuário e senha para /auth/login uma vez e guarda o token resultante durante a vida do engine. As credenciais não são reenviadas a cada consulta.
DB-API 2.0¶
Interface padrão PEP 249. Funciona com qualquer ferramenta que aceite uma conexão DB-API.
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 envia usuário e senha para /auth/login e guarda o access_token que recebe, então a conexão carrega uma credencial real. role solicita um papel, e o servidor só o concede se a identidade tiver esse papel atribuído. Se você omitir, a conexão roda com o papel que o login resolveu.
Executando consultas¶
O cursor aceita GraphQL ou SQL e detecta qual recebeu.
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()
Metadados de coluna¶
cur.execute("{ orders { id amount } }")
print(cur.description)
# [('id', None, ...), ('amount', None, ...)]
print(cur.rowcount)
Parâmetros nomeados¶
Gerenciadores de contexto¶
with connect("http://localhost:8001", username="alice", password="secret") as conn:
with conn.cursor() as cur:
cur.execute("{ orders { id amount } }")
print(cur.fetchall())
Dialeto SQLAlchemy¶
Esquema de URL: provisa+http:// ou provisa+https://
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)
Com pandas¶
Parâmetros de URL¶
| Parâmetro | Descrição | Padrão |
|---|---|---|
role |
Função a solicitar; validada no servidor | a função que o login resolve |
Introspecção de esquema¶
O dialeto reflete o catálogo servido ao seu papel, lido de GET /data/catalog como o usuário, o papel e a organização da conexão. Cada domínio é um esquema, e cada tabela vive em um: get_schema_names() lista os domínios, get_table_names(schema="shop") lista as tabelas desse domínio pelos seus nomes SQL, e get_columns("orders", schema="shop") dá o tipo, a nulidade e a descrição de cada coluna. Sem esquema nomeado, get_table_names() não lista nenhuma. Uma tabela desconhecida gera NoSuchTableError; um catálogo que o servidor recusa gera um erro.
ADBC¶
Arrow Database Connectivity, apoiado no Arrow Flight. Devolve uma pyarrow.Table diretamente, sem desserialização de JSON.
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
)
adbc_connect faz login por HTTP primeiro e coloca o token resultante em cada ticket do Flight, então o servidor Flight autentica a conexão do mesmo jeito que a superfície REST. O argumento role é uma solicitação que o servidor valida contra as atribuições da identidade. Ele nunca vira a identidade.
Buscar como Arrow Table¶
with conn.cursor() as cur:
cur.execute("{ orders { id amount region } }")
table = cur.fetch_arrow_table() # pyarrow.Table
df = table.to_pandas()
Buscar como tuplas¶
with conn.cursor() as cur:
cur.execute("{ orders { id amount } }")
rows = cur.fetchall() # list of tuples
one = cur.fetchone() # single tuple or None
Metadados de coluna¶
cur.execute("{ orders { id amount } }")
print(cur.description)
# [('id', None, ...), ('amount', None, ...)]
Gerenciador de contexto¶
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()
O ADBC se conecta ao servidor Flight na porta 8815 por padrão. Passe port= para alcançar um servidor Flight vinculado a uma porta não padrão.