לדלג לתוכן

לקוח Python (provisa-client)

לקוח Python עבור Provisa. מספק ארבע ממשקים:

ממשק מקרה שימוש
ProvisaClient שאילתות GraphQL, Arrow Flight, פלט DataFrame
DB-API 2.0 (connect) ממשק מסד נתונים סטנדרטי של Python (PEP 249) (REQ-268)
דיאלקט SQLAlchemy כלי BI, ORM, read_sql‏ Pandas (REQ-270)
ADBC סטרימינג עמודתי ילידי-Arrow דרך Flight (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.

כדי להמיר סיסמה לאסימון, שלחו 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

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

Arrow Flight (עמודתי בעל-תפוקה-גבוהה)

השתמשו ב-Flight עבור סטי תוצאות גדולים — נתונים זורמים בסטרימינג כ-batches‏ רשומה של Arrow ללא מימוש בשרת. (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=:

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

סקירת קטלוג

tables_df = client.list_tables()

רפרנס חיבור

פרמטר ברירת מחדל תיאור
url http://localhost:8001 URL בסיס של שרת Provisa
token None אישור bearer — אסימון של הספק או אסימון גישה אישי; השמיטו עבור אימות סיסמה (REQ-606, REQ-1263)
role None התפקיד שבשמו לפעול, נשלח כ-X-Provisa-Role ומכובד רק אם הוא מוקצה למשתמש; אם הוא מושמט, הבקשה רצה בתפקיד של הזהות עצמה (REQ-273)
flight_port 8815 פורט gRPC של Arrow Flight (REQ-143)

טיפול בשגיאות

query() מעלה httpx.HTTPStatusError על שגיאות HTTP. (REQ-607) query_df() מעלה RuntimeError אם התגובה מכילה שגיאות GraphQL. (REQ-607)


שימוש ב-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(...).

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. לטבלאות רחבות או לתוצאות של יותר מכמה מאות אלפי שורות, זה מהיר יותר וצורך פחות זיכרון.

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://. שימו את אישורי הגישה במשתני סביבה, לא במחרוזת ה-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)

פרמטרים בעלי-שם

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

מנהלי הקשר (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

pip install "provisa-client[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

import pandas as pd

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

פרמטרי URL

פרמטר תיאור ברירת מחדל
role התפקיד המבוקש; מאומת בצד השרת (REQ-273) התפקיד שההתחברות פותרת
engine = create_engine(
    "provisa+http://alice:secret@localhost:8001?role=analyst"
)

Introspection סכמה

הדיאלקט משקף את הקטלוג שמוגש לתפקיד שלכם, נקרא מ-GET /data/catalog כמשתמש, התפקיד והארגון של החיבור. כל דומיין הוא סכימה, וכל טבלה נמצאת באחת: get_schema_names() מציג את הדומיינים, get_table_names(schema="shop") מציג את טבלאות הדומיין לפי שמות ה-SQL שלהן, ו-get_columns("orders", schema="shop") נותן את הסוג, האפשרות ל-NULL והתיאור של כל עמודה. בלי סכימה שצוינה, get_table_names() אינו מציג אף אחת. טבלה לא מוכרת מעלה NoSuchTableError; קטלוג שהשרת מסרב לו מעלה שגיאה. (REQ-363, REQ-270, REQ-2070)


ADBC

Arrow Database Connectivity מגובה Arrow Flight. (REQ-271) מחזיר pyarrow.Table ישירות — ללא דה-סריאליזציית JSON. (REQ-271)

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

adbc_connect מתחבר תחילה מעל HTTP ומשבץ את האסימון שהתקבל בכל כרטיס Flight, כך ששרת ה-Flight מאמת את החיבור בדיוק כפי שממשק ה-REST עושה זאת. (REQ-1263) הארגומנט role הוא בקשה, המאומתת בצד השרת מול ההקצאות של הזהות — הוא לעולם אינו הופך לזהות. (REQ-273)

שליפה כטבלת 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 כברירת מחדל. (REQ-143) העבירו port= כדי להגיע לשרת Flight המקושר לפורט לא-ברירת-מחדל. (REQ-711)