varianttests.notebook_runtime
spark_conf_applied
@contextlib.contextmanager
def spark_conf_applied(ctx: StepContext, settings: dict[str, str])
Zet sessievlaggen voor deze ene stap en draai ze daarna terug.
De batchnotebook draait tientallen scenario's in dezelfde Spark-sessie, dus
een vlag die blijft staan verandert het gedrag van elk scenario erna: een
rode uitslag verderop is dan niet meer te verklaren uit het scenario dat
faalt. Vandaar terugzetten in een finally, ook als de load klapt, en
unset voor een instelling die er eerst niet stond.
bind_check_sql
def bind_check_sql(sql: str, bindings: dict) -> str
Vul de plaatshouders van een check-query in, of weiger de query.
Meta.dbo.logging is van de hele suite samen en wordt tijdens een run door niets geleegd. Een check zonder binding leest daarom ook de regels van elk scenario dat eerder langskwam, en bij meerdere leveringen die van zijn eigen vorige levering. Dat is niet zichtbaar in isolatie en wel in een volle batch. Een onbekende plaatshouder wordt hier een fout, geen stille letterlijke tekst, want een check die het verkeerde meet is erger dan een check die rood wordt.
check_bindings
def check_bindings(ctx: StepContext) -> dict
De scopes die een check-query mag noemen.
run_id is de scherpste: vers per load, dus hij scheidt ook de leveringen
binnen een scenario. Een hooknotebook draait als eigen notebook-run en logt
onder zijn eigen run_id, dus daar is hij onbruikbaar. batch_id wel: de
aanroeper geeft zijn activity_id door als base_batch_id, en de hook logt
zijn regels daaronder. Dat scheidt runs van elkaar, niet scenario's
onderling: zwakker dan run_id, en nog altijd een regel in plaats van niets.
dax_request
def dax_request(params: dict) -> dict
Read a dax step's settings, wherever the caller could put them.
Two callers, and they do not carry parameters equally far. Variant_Suite
hands the params from the plan straight to run_step, so anything fits.
Variant_Runner builds its step params from a fixed list of declared
notebook parameters and silently drops the rest; that list lives in
DemoPlatform. Build 1396 hit exactly that as KeyError: 'model'.
So the settings may also travel inside checks_json, which both callers do
forward. One shape is read either way, and no second repository has to change
for a step that is defined here.
xmla_connection_string
def xmla_connection_string(workspace: str,
model: str,
token: str,
effective_user_name: str | None = None,
role: str | None = None) -> str
Bouw de XMLA-verbinding zelf, zonder ook maar iets op te zoeken.
Het endpoint accepteert alleen de workspacenaam, geen id. Daar liep sempy op
stuk: het kreeg van ons een id, moest er een naam bij zoeken en deed dat via
GET /v1.0/myorg/groups — gesloten voor een service principal, vier keer
403 (build 1419, 1428, 1435, 1441). Wie de naam al heeft, hoeft niets te
vragen.
EffectiveUserName is de impersonatie van Analysis Services zelf en vraagt
beheerrechten op het model; Roles kiest een rol zonder gebruiker. Ze
sluiten elkaar uit.
json_safe
def json_safe(value: Any)
Maak een XMLA-waarde verplaatsbaar naar de agent, die alleen JSON krijgt.
De notebook geeft zijn payload terug als JSON. Een measure met decimalen komt over XMLA terug als een decimaal, en daar brak json.dumps op: build 1453 en 1455 verloren er hun hele Spark-sessie aan, met de assertie ongemeten. Een COUNTROWS gaat wel goed, dus het kwam pas boven bij de eerste stap die een measure las.
Het is een System.Decimal uit .NET, en pythonnet maakt daar geen decimal.Decimal van. Build 1461 liet dat zien: de assertie kreeg de string '30.00' waar 30.0 werd verwacht. Wat er getallig uitziet gaat daarom door float heen, ongeacht van welke kant het komt.
Wat daarna nog overblijft wordt een string in plaats van een uitzondering. Een onverwacht type levert dan een zichtbaar verkeerde assertie op, en niet een gesneuvelde sessie zonder uitslag.
fabric_request
def fabric_request(method: str,
url: str,
token: str,
body: bytes | None = None) -> tuple[int, dict, str]
Een kale REST-aanroep tegen de Fabric-API, zonder de wheel-client.
Deze module wordt als los bestand naar de notebook gekopieerd en daar
standalone uitgevoerd - build 1619 brak op from .fabric_client import
met "attempted relative import with no known parent package". Alles wat hier
staat moet het dus met de standaardbibliotheek redden.
sync_sql_endpoint
def sync_sql_endpoint(
endpoint: dict,
token: str | None = None,
request: Callable = fabric_request,
sleep: Callable[[float], None] = time.sleep,
clock: Callable[[], float] = time.monotonic) -> list[dict]
Breng de metadata van een lakehouseA place where you store both "raw" data (like files) and "organized" data (like tables). It combines the best of a File Cabinet and a Database.-SQL-endpoint bij.
Een semantisch model importeert via het endpoint, en dat ziet een verse Spark-write pas na deze sync. De agent deed hem tot nu toe vlak voor de refresh; nu de refresh in de notebook zit, hoort de sync mee te verhuizen, anders refresht het model op oude metadata.
run_tabular_step
def run_tabular_step(checks: dict,
model: str,
workspace: str | None = None,
refresh: bool = False,
endpoint: dict | None = None,
execute: Callable | None = None,
sync: Callable[[dict], list] | None = None,
evaluate: Callable | None = None,
diagnose: dict | None = None) -> dict
Refresh een semantisch model vanuit de notebook en lees het daarna uit.
De refresh liep tot build 1594 client-side over de enhanced-refresh-API van Power BI. Dat was een omweg: de notebook heeft de XMLA-verbinding al voor zijn DAX-checks, en een TMSL-refresh is hetzelfde commando-object met ExecuteNonQuery in plaats van ExecuteReader. Het scheelt niet alleen een aparte Fabric-job per tabular-stap, het haalt ook het enige pad weg dat op 403 TokenExpired kon stuklopen (#614).
log_status houdt de waarden aan die de client-side route teruggaf, want
scenario's asserteren erop: completed na een geslaagde refresh en
skipped zonder. XMLA kent geen tussenstand - ExecuteNonQuery keert terug
of werpt.
run_dax_checks
def run_dax_checks(checks: dict,
model: str,
workspace: str | None = None,
effective_user_name: str | None = None,
role: str | None = None,
evaluate: Callable | None = None,
diagnose: dict | None = None) -> dict
Run DAX against a semantic modelThe "brain" of your data that tells Power BI how different pieces of information relate to each other. from inside the notebook, over XMLA.
Not over the executeQueries REST API, which the agent reaches more easily. That route is closed for this suite: the documentation of Execute Queries states that service principals are not supported for datasets with RLS, whatever the tenant settings say, and the suite authenticates as one. Build 1366 showed it as HTTP 401 on every query against the VT model, including the one without impersonation.
sempy is de tweede route die afvalt: het zoekt de workspace altijd op over een API die voor een service principal dicht zit, en komt zo nooit bij XMLA aan. Daarom praat deze code zelf met het endpoint.
A failing query becomes an ERROR string rather than an exception, because a scenario asserts on exactly that for a table a role may not read.
Met column: true komt de hele eerste kolom terug, samengevoegd tot een
string. Een DMV als $SYSTEM.TMSCHEMA_ROLE_MEMBERSHIPS heeft een rij per
lid, en de eerste cel zou de rest verzwijgen. Een string omdat de assertie
contains: daarop werkt.