לקוח Python (provisa-client)¶
לקוח Python עבור Provisa. מספק ארבע ממשקים:
| ממשק | מקרה שימוש |
|---|---|
ProvisaClient |
שאילתות GraphQL, Arrow Flight, פלט DataFrame |
DB-API 2.0 (connect) |
ממשק מסד נתונים סטנדרטי של Python (PEP 249) |
| דיאלקט SQLAlchemy | כלי BI, ORM, read_sql Pandas |
| 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 } }")
Async¶
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, מחוץ לקובץ. כניסה שנדחתה, כתובת חסרה או פרופיל בשם שאינו בקובץ מעלים שגיאה עם הסיבה. ל-Provisa אין תהליך כניסה ש-notebook יכול להפעיל דרך דפדפן, ולכן בפריסות עם כניסה יחידה השתמשו באסימון גישה אישי.
פקודות קסם לתאים¶
%%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(...).
DataFrames מ-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 עם כתובת provisa+http://. קראו את פרטי ההתחברות ממשתני סביבה, כדי שלעולם לא יופיעו ב-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)
הדיאלקט שולח את שם המשתמש והסיסמה אל /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)
פרמטרים בעלי-שם¶
מנהלי הקשר (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¶
סכמת 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 |
התפקיד המבוקש; מאומת בצד השרת | התפקיד שההתחברות פותרת |
Introspection סכמה¶
הדיאלקט משקף את הקטלוג שמוגש לתפקיד שלכם, נקרא מ-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()
שליפה כ-tuples¶
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 מתחבר לשרת ה-Flight על פורט 8815 כברירת מחדל. העבירו port= כדי להגיע לשרת Flight המקושר לפורט לא-ברירת-מחדל.