Перейти к содержанию

Python-клиент (provisa-client)

Python-клиент для Provisa. Предоставляет четыре интерфейса:

Интерфейс Сценарий использования
ProvisaClient Запросы GraphQL, Arrow Flight, вывод DataFrame
DB-API 2.0 (connect) Стандартный интерфейс базы данных Python (PEP 249)
Диалект SQLAlchemy BI-инструменты, ORM, Pandas read_sql
ADBC Нативная колоночная потоковая передача Arrow через Flight

Установка

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

Быстрый старт

from provisa_client import ProvisaClient

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

ProvisaClient принимает учётные данные. Собственного шага входа у него нет, поэтому он никогда не спрашивает имя пользователя и пароль. Когда скрипт должен работать без присмотра, используйте персональный токен доступа: вы выпускаете его в своём профиле, у него есть срок действия, и его можно отозвать, не трогая учётную запись. Токен bearer провайдера работает так же. Любой из них передаётся в token, и клиент предъявляет его и на пути HTTP, и на пути Arrow Flight.

Чтобы обменять пароль на токен, отправьте POST на /auth/login и прочитайте 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"])

Точки входа DB-API и ADBC выполняют этот обмен за вас, как описано ниже.

Запросы 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 } }")

Асинхронность

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

Arrow Flight (высокая пропускная способность, колоночный формат)

Используйте Flight для больших наборов результатов. Данные передаются потоком пакетов записей Arrow и никогда не материализуются на сервере.

import pyarrow as pa

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

Flight по умолчанию подключается к порту 8815. Переопределите через flight_port=:

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

Исследование каталога

tables_df = client.list_tables()

Справочник подключения

Параметр По умолчанию Описание
url http://localhost:8001 Базовый URL сервера Provisa
token None Bearer-учётные данные — токен провайдера или персональный токен доступа; опустите для аутентификации по паролю
role None Роль, от имени которой действовать; передаётся как X-Provisa-Role и учитывается, только если назначена пользователю; если не указана, запрос выполняется от собственной роли идентичности
flight_port 8815 Порт gRPC Arrow Flight

Обработка ошибок

query() выбрасывает httpx.HTTPStatusError при ошибках HTTP. query_df() выбрасывает RuntimeError, если ответ содержит ошибки GraphQL.


Provisa в Jupyter

Установите пакет в работающее ядро до импорта. Дополнение pandas даёт вывод в DataFrame; добавьте sqlalchemy, если также нужен pandas.read_sql через диалект.

%pip install "provisa-client[pandas]"

Учётные данные

Блокноты сохраняют вывод ячеек в файл .ipynb. Токен, введённый в ячейку, будет сохранён на диск вместе с вашим кодом. Не делайте так.

ProvisaClient не читает окружение: передайте ему учётные данные явно, из переменных окружения. (provisa_client.query(), ниже, читает их за вас.)

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
)

Задайте эти переменные в оболочке до запуска Jupyter или загрузите файл .env через python-dotenv. В любом случае держите их вне блокнота.

Один вызов: query()

provisa_client.query() принимает инструкцию SQL, GraphQL или Cypher и возвращает DataFrame pandas. Сервер сам определяет язык. Заранее ничего не создаётся: соединение берётся из окружения или из файла профиля.

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= выполняет одно чтение от имени этой роли и отправляется как X-Provisa-Role. Сервер принимает его только для роли, которая у вас есть. Чтение идёт через Arrow Flight; задайте transport равным http, чтобы отправить его на /data/query. await provisa_client.aquery(...) — асинхронная форма.

Соединение читается из первого места, где названа каждая настройка:

  1. ключевые аргументы provisa_client.sign_in(...), который один раз разрешает соединение для всех последующих чтений;
  2. переменные окружения: PROVISA_URL, PROVISA_TOKEN, PROVISA_USERNAME, PROVISA_PASSWORD, PROVISA_ROLE, PROVISA_ORG, PROVISA_FLIGHT_PORT, PROVISA_TRANSPORT;
  3. раздел файла ~/.provisa/credentials: [default] или тот, что назван в PROVISA_PROFILE (или sign_in(profile=...)).
[default]
url = https://provisa.example.com
token = provisa_pat_...
role = analyst

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

token является персональным токеном доступа или токеном сеанса. Если вместо него передать username и password, клиент обменяет их на токен сеанса через POST /auth/login. Это работает в развёртывании, где провайдер входа принимает пароль (basic, simple, LDAP). Храните пароль в PROVISA_PASSWORD, а не в файле. Отклонённый вход, отсутствующий URL или именованный профиль, которого нет в файле, вызывают ошибку с причиной. У Provisa нет процесса входа, который блокнот мог бы провести через браузер, поэтому в развёртываниях с единым входом используйте персональный токен доступа.

Магические команды ячеек

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

%%sql, %%graphql и %%cypher отвечают на ячейку через query(). DataFrame — результат ячейки. Имя после магической команды также связывает его в блокноте (eu выше); --role выполняет ячейку от имени этой роли.

JupySQL (%load_ext sql) тоже определяет %%sql. Загрузите оба — и %%sql достанется расширению, загруженному последним. Расширение Provisa принимает --role и читает через Arrow Flight. JupySQL тоже работает, через диалект SQLAlchemy: %sql provisa+http://user:password@host:port подключается, %sql/%%sql выполняют управляемые инструкции, а %sqlcmd tables -s <domain> и %sqlcmd columns -t <table> -s <domain> выводят каталог.

В браузере (Pyodide)

В Pyodide query() читает с сервера, который отдал страницу, от имени пользователя, вошедшего на странице. Страница-хост хранит этого пользователя в глобальной области ядра:

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

baseUrl пуст для собственного источника страницы. Клиент читает provisaAuth при каждом запросе, поэтому обновлённый токен действует без перезапуска. Результаты приходят в Arrow, когда загружен pyarrow; без него — в JSON. Инструкция Cypher всегда возвращается в JSON. query() блокирует выполнение, что может Web Worker (где работает ядро блокнота); в основном потоке страницы он вызывает ошибку, и нужно использовать await provisa_client.aquery(...).

DataFrame из GraphQL, SQL и Cypher

query_df() принимает все три языка запросов и возвращает DataFrame 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")

Ответы GraphQL несут данные под именем корневого поля; query_df() извлекает их. Ответы SQL и Cypher содержат явные имена столбцов от сервера и отображаются напрямую.

Arrow Flight для больших выборок

flight_df() передаёт данные потоком пакетов записей Arrow вместо десериализации JSON. Для широких таблиц или результатов более чем в несколько сотен тысяч строк это быстрее и требует меньше памяти.

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

Arrow сохраняет типы столбцов. Целые остаются целыми, а метки времени остаются метками времени, чего JSON обещать не может. Когда схема важна, одного этого достаточно, чтобы предпочесть Flight.

Flight по умолчанию подключается к порту 8815. Передайте flight_port= в конструктор ProvisaClient, если ваш сервер использует другой порт.

pandas.read_sql через диалект SQLAlchemy

Установите дополнение sqlalchemy в ядро:

%pip install "provisa-client[sqlalchemy]"

Затем создайте engine с URL provisa+http://. Читайте учётные данные из переменных окружения, чтобы они никогда не оказались в блокноте.

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)

Диалект один раз отправляет имя пользователя и пароль на /auth/login и хранит полученный токен всё время жизни engine. С каждым запросом учётные данные повторно не отправляются.


DB-API 2.0

Стандартный интерфейс PEP 249. Работает с любым инструментом, принимающим подключение 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 отправляет имя пользователя и пароль на /auth/login и хранит полученный access_token, поэтому соединение несёт настоящие учётные данные. role запрашивает роль, и сервер выполняет запрос, только если эта роль назначена идентичности. Если параметр опустить, соединение работает с ролью, определённой при входе.

Выполнение запросов

Курсор принимает GraphQL или SQL и сам определяет, что получил.

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()

Метаданные столбцов

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

Именованные параметры

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

Контекстные менеджеры

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

pip install "provisa-client[sqlalchemy]"

Схема URL: provisa+http:// или 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)

С pandas

import pandas as pd

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

Параметры URL

Параметр Описание По умолчанию
role Запрашиваемая роль; проверяется на сервере роль, которую разрешает вход
engine = create_engine(
    "provisa+http://alice:secret@localhost:8001?role=analyst"
)

Интроспекция схемы

Диалект отражает каталог, который отдаётся вашей роли, читая его из GET /data/catalog от имени пользователя, роли и организации соединения. Каждый домен — это схема, и каждая таблица находится в одной из них: get_schema_names() выводит домены, get_table_names(schema="shop") — таблицы этого домена по их SQL-именам, а get_columns("orders", schema="shop") даёт тип, допустимость NULL и описание каждого столбца. Без указанной схемы get_table_names() не выводит ни одной. Неизвестная таблица вызывает NoSuchTableError; каталог, в котором отказывает сервер, вызывает ошибку.


ADBC

Arrow Database Connectivity на основе Arrow Flight. Возвращает pyarrow.Table напрямую, без десериализации JSON.

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
)

adbc_connect сначала входит по HTTP и кладёт полученный токен в каждый билет Flight, поэтому сервер Flight аутентифицирует соединение так же, как поверхность REST. Аргумент role является запросом, который сервер проверяет по назначениям идентичности. Он никогда не становится идентичностью.

Получение как таблицы Arrow

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

Получение как кортежей

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

Метаданные столбцов

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

Контекстный менеджер

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 по умолчанию подключается к серверу Flight на порту 8815. Передайте port=, чтобы обратиться к серверу Flight, привязанному к нестандартному порту.