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 } }")
Асинхронность¶
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=:
Исследование каталога¶
Справочник подключения¶
| Параметр | По умолчанию | Описание |
|---|---|---|
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 через диалект.
Учётные данные¶
Блокноты сохраняют вывод ячеек в файл .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(...) — асинхронная форма.
Соединение читается из первого места, где названа каждая настройка:
- ключевые аргументы
provisa_client.sign_in(...), который один раз разрешает соединение для всех последующих чтений; - переменные окружения:
PROVISA_URL,PROVISA_TOKEN,PROVISA_USERNAME,PROVISA_PASSWORD,PROVISA_ROLE,PROVISA_ORG,PROVISA_FLIGHT_PORT,PROVISA_TRANSPORT; - раздел файла
~/.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 нет процесса входа, который блокнот мог бы провести через браузер, поэтому в развёртываниях с единым входом используйте персональный токен доступа.
Магические команды ячеек¶
%%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() читает с сервера, который отдал страницу, от имени пользователя, вошедшего на странице. Страница-хост хранит этого пользователя в глобальной области ядра:
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. Для широких таблиц или результатов более чем в несколько сотен тысяч строк это быстрее и требует меньше памяти.
Arrow сохраняет типы столбцов. Целые остаются целыми, а метки времени остаются метками времени, чего JSON обещать не может. Когда схема важна, одного этого достаточно, чтобы предпочесть Flight.
Flight по умолчанию подключается к порту 8815. Передайте flight_port= в конструктор ProvisaClient, если ваш сервер использует другой порт.
pandas.read_sql через диалект SQLAlchemy¶
Установите дополнение 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)
Именованные параметры¶
Контекстные менеджеры¶
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¶
Схема 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¶
Параметры URL¶
| Параметр | Описание | По умолчанию |
|---|---|---|
role |
Запрашиваемая роль; проверяется на сервере | роль, которую разрешает вход |
Интроспекция схемы¶
Диалект отражает каталог, который отдаётся вашей роли, читая его из 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.
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, привязанному к нестандартному порту.