Python 用戶端(provisa-client)¶
Provisa 的 Python 用戶端。提供四種介面:
| 介面 | 使用場景 |
|---|---|
ProvisaClient |
GraphQL 查詢、Arrow Flight、DataFrame 輸出 |
DB-API 2.0(connect) |
標準 Python 資料庫介面(PEP 249)(REQ-268) |
| SQLAlchemy 方言 | BI 工具、ORM、Pandas read_sql (REQ-270) |
| ADBC | 經 Flight 的 Arrow 原生欄式串流 (REQ-271) |
安裝¶
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 接受的是一份憑證,而不是用戶名加密碼:它本身沒有登入步驟。當指令碼需要無人看管地執行時,個人存取權杖是首選憑證——它由用戶自己的個人資料頁簽發,帶有有效期,並且可以在不動帳戶的情況下撤銷。(REQ-1263) 提供者簽發的 bearer 權杖用法完全相同。兩者都放在 token 中,客戶端會在 HTTP 和 Arrow Flight 兩條路徑上都出示它。
若要用密碼換取權杖,向 /auth/login 發送 POST 並讀取 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 record batch 形式串流,不會於伺服端具體化。(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 預設連往連接埠 8815。(REQ-143) 可以 flight_port= 覆寫:
目錄探索¶
連線參考¶
| 參數 | 預設值 | 描述 |
|---|---|---|
url |
http://localhost:8001 |
Provisa 伺服器基礎 URL |
token |
None |
Bearer 憑證——提供者權杖或個人存取權杖;若採密碼驗證則留空 (REQ-606、REQ-1263) |
role |
None |
要代為執行的角色,以 X-Provisa-Role 傳送,只有在該角色已指派給使用者時才會生效;省略時,請求以身份本身的角色執行 (REQ-273) |
flight_port |
8815 |
Arrow Flight gRPC 連接埠 (REQ-143) |
錯誤處理¶
query() 於 HTTP 錯誤時擲出 httpx.HTTPStatusError。(REQ-607)
query_df() 於回應含有 GraphQL 錯誤時擲出 RuntimeError。(REQ-607)
在 Jupyter 中使用 Provisa¶
在匯入之前,先安裝到正在執行的核心中。pandas 擴充提供 DataFrame 輸出;如果還需要透過方言使用 pandas.read_sql,請加上 sqlalchemy。
憑證¶
筆記本會把儲存格輸出儲存到 .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 之前在 shell 中設定這些變數,或用 python-dotenv 載入 .env 檔案。無論哪種方式,都不要讓它們出現在筆記本中。
一次呼叫:query()¶
provisa_client.query() 接受一條 SQL、GraphQL 或 Cypher 陳述式,傳回一個 pandas DataFrame。由伺服器判斷是哪種語言。無需事先建構任何物件:連線來自環境或設定檔。
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,因此輪換後的權杖無需重新啟動即可生效。載入了 pyarrow 時結果以 Arrow 傳回;否則以 JSON 傳回。Cypher 陳述式總是以 JSON 傳回。query() 是阻塞呼叫,Web Worker(筆記本核心執行的地方)可以這樣做;在頁面主執行緒上它會拋出錯誤,應改用 await provisa_client.aquery(...)。
從 GraphQL、SQL 和 Cypher 取得 DataFrame¶
query_df() 接受這三種查詢語言,並傳回 pandas DataFrame。
# 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 連接埠。如果伺服器使用其他連接埠,請向 ProvisaClient 建構函式傳入 flight_port=。
透過 SQLAlchemy 方言使用 pandas.read_sql¶
把 sqlalchemy 擴充安裝到核心中:
然後用 provisa+http:// URL 建立 engine。把憑證放在環境變數中,而不是 URL 字串裡。
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 介面。(REQ-268) 適用於任何接受 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 會把用戶名和密碼 POST 至 /auth/login,並保留回傳的 access_token,因此該連線攜帶的是一份真正的憑證,而不只是一個名字。role 是在請求某個角色,只有該身分確實獲配該角色時伺服器才會予以滿足 (REQ-273);若省略,連線就以登入所解析出的角色執行。
執行查詢¶
游標接受 GraphQL 或 SQL——會自動偵測。(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()
欄位中繼資料¶
cur.execute("{ orders { id amount } }")
print(cur.description)
# [('id', None, ...), ('amount', None, ...)]
print(cur.rowcount)
具名參數¶
Context Manager¶
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:// (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)
搭配 pandas¶
URL 參數¶
| 參數 | 描述 | 預設值 |
|---|---|---|
role |
要請求的角色;由伺服器端驗證 (REQ-273) | 登入所解析出的角色 |
結構描述內省¶
方言反映提供給你的角色的目錄,以連線的使用者、角色和組織身分從 GET /data/catalog 讀取。每個網域是一個結構描述,每張資料表都位於其中之一:get_schema_names() 列出各個網域,get_table_names(schema="shop") 按 SQL 名稱列出該網域的資料表,get_columns("orders", schema="shop") 給出每個欄位的類型、可空性和描述。未指定結構描述時,get_table_names() 不列出任何資料表。未知的資料表會拋出 NoSuchTableError;伺服器拒絕的目錄會拋出錯誤。(REQ-363, REQ-270, REQ-2070)
ADBC¶
以 Arrow Flight 為後盾的 Arrow Database Connectivity。(REQ-271) 直接回傳 pyarrow.Table——無需 JSON 反序列化。(REQ-271)
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 會先透過 HTTP 登入,並把取得的權杖放入每一張 Flight ticket,因此 Flight 伺服器對連線的驗證方式與 REST 介面完全一致。(REQ-1263) role 參數是一項請求,由伺服器對照該身分的角色指派進行驗證——它絕不會成為身分本身。(REQ-273)
擷取為 Arrow Table¶
with conn.cursor() as cur:
cur.execute("{ orders { id amount region } }")
table = cur.fetch_arrow_table() # pyarrow.Table
df = table.to_pandas()
擷取為 tuple¶
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, ...)]
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 預設連往連接埠 8815 上的 Flight 伺服器。(REQ-143) 傳入 port= 可連往綁定於非預設連接埠的 Flight 伺服器。(REQ-711)