import urllib .request
import urllib .error
import json
# --- CONFIG ---
WORKSPACE_ID = "your-workspace-id-here"
DATASET_OLD = "your-old-dataset-id"
DATASET_NEW = "your-new-dataset-id"
ACCESS_TOKEN = "your-bearer-token"
XMLA_BASE = "https://a...content-available-to-author-only...s.net/powerbi/api/v1.0/myorg/groups/{}/datasets" .format ( WORKSPACE_ID)
THRESHOLD = 0.0001 # 0.01% delta
MEASURES = [
{ "name" : "Total Revenue" , "dax" : 'EVALUATE ROW("val", [Total Revenue])' } ,
{ "name" : "Gross Margin %" , "dax" : 'EVALUATE ROW("val", [Gross Margin %])' } ,
{ "name" : "Active Customers" , "dax" : 'EVALUATE ROW("val", [Active Customers])' } ,
{ "name" : "YoY Growth" , "dax" : 'EVALUATE ROW("val", [YoY Growth])' } ,
]
def query_dataset( dataset_id, dax_query) :
url = "{}/{}/executeQueries" .format ( XMLA_BASE, dataset_id)
headers = {
"Authorization" : "Bearer " + ACCESS_TOKEN,
"Content-Type" : "application/json"
}
body = json.dumps ( {
"queries" : [ { "query" : dax_query} ] ,
"serializerSettings" : { "includeNulls" : True }
} ) .encode ( "utf-8" )
req = urllib .request .Request ( url, data= body, headers= headers, method= "POST" )
try :
with urllib .request .urlopen ( req) as resp:
data = json.loads ( resp.read ( ) .decode ( "utf-8" ) )
rows = data[ "results" ] [ 0 ] [ "tables" ] [ 0 ] [ "rows" ]
return rows[ 0 ] [ "[val]" ]
except ( urllib .error .URLError , KeyError , IndexError ) :
return None
def compare_measures( ) :
results = [ ]
for m in MEASURES:
old_val = query_dataset( DATASET_OLD, m[ "dax" ] )
new_val = query_dataset( DATASET_NEW, m[ "dax" ] )
if old_val is None or new_val is None :
status = "ERROR"
delta_str = "N/A"
else :
try :
old_f = float ( old_val)
new_f = float ( new_val)
if old_f == 0 :
delta = abs ( new_f)
else :
delta = abs ( ( new_f - old_f) / old_f)
status = "MISMATCH" if delta > THRESHOLD else "OK"
delta_str = "{:.4%}" .format ( delta)
except ( ValueError , TypeError ) :
delta_str = "N/A"
status = "ERROR"
results.append ( {
"measure" : m[ "name" ] ,
"old" : old_val,
"new" : new_val,
"delta" : delta_str,
"status" : status
} )
return results
if __name__ == "__main__" :
print ( "=== DAX Measure Comparison ===\n " )
print ( "{:<20} {:<15} {:<15} {:<12} {}" .format (
"Measure" , "Old Model" , "New Model" , "Delta" , "Status" ) )
print ( "-" * 75 )
rows = compare_measures( )
mismatch_count = 0
for r in rows:
print ( "{:<20} {:<15} {:<15} {:<12} {}" .format (
r[ "measure" ] ,
str ( r[ "old" ] ) ,
str ( r[ "new" ] ) ,
r[ "delta" ] ,
r[ "status" ] ) )
if r[ "status" ] == "MISMATCH" :
mismatch_count += 1
if mismatch_count > 0 :
print ( "\n ! {} measure(s) exceed {:.2%} threshold" .format (
mismatch_count, THRESHOLD) )
else :
print ( "\n All measures within threshold" ) # your code goes here
aW1wb3J0IHVybGxpYi5yZXF1ZXN0CmltcG9ydCB1cmxsaWIuZXJyb3IKaW1wb3J0IGpzb24KCiMgLS0tIENPTkZJRyAtLS0KV09SS1NQQUNFX0lEID0gInlvdXItd29ya3NwYWNlLWlkLWhlcmUiCkRBVEFTRVRfT0xEID0gInlvdXItb2xkLWRhdGFzZXQtaWQiCkRBVEFTRVRfTkVXID0gInlvdXItbmV3LWRhdGFzZXQtaWQiCkFDQ0VTU19UT0tFTiA9ICJ5b3VyLWJlYXJlci10b2tlbiIKWE1MQV9CQVNFID0gImh0dHBzOi8vYS4uLmNvbnRlbnQtYXZhaWxhYmxlLXRvLWF1dGhvci1vbmx5Li4ucy5uZXQvcG93ZXJiaS9hcGkvdjEuMC9teW9yZy9ncm91cHMve30vZGF0YXNldHMiLmZvcm1hdChXT1JLU1BBQ0VfSUQpClRIUkVTSE9MRCA9IDAuMDAwMSAgIyAwLjAxJSBkZWx0YQoKTUVBU1VSRVMgPSBbCiAgICB7Im5hbWUiOiAiVG90YWwgUmV2ZW51ZSIsICJkYXgiOiAnRVZBTFVBVEUgUk9XKCJ2YWwiLCBbVG90YWwgUmV2ZW51ZV0pJ30sCiAgICB7Im5hbWUiOiAiR3Jvc3MgTWFyZ2luICUiLCAiZGF4IjogJ0VWQUxVQVRFIFJPVygidmFsIiwgW0dyb3NzIE1hcmdpbiAlXSknfSwKICAgIHsibmFtZSI6ICJBY3RpdmUgQ3VzdG9tZXJzIiwgImRheCI6ICdFVkFMVUFURSBST1coInZhbCIsIFtBY3RpdmUgQ3VzdG9tZXJzXSknfSwKICAgIHsibmFtZSI6ICJZb1kgR3Jvd3RoIiwgImRheCI6ICdFVkFMVUFURSBST1coInZhbCIsIFtZb1kgR3Jvd3RoXSknfSwKXQoKZGVmIHF1ZXJ5X2RhdGFzZXQoZGF0YXNldF9pZCwgZGF4X3F1ZXJ5KToKICAgIHVybCA9ICJ7fS97fS9leGVjdXRlUXVlcmllcyIuZm9ybWF0KFhNTEFfQkFTRSwgZGF0YXNldF9pZCkKICAgIGhlYWRlcnMgPSB7CiAgICAgICAgIkF1dGhvcml6YXRpb24iOiAiQmVhcmVyICIgKyBBQ0NFU1NfVE9LRU4sCiAgICAgICAgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIgogICAgfQogICAgYm9keSA9IGpzb24uZHVtcHMoewogICAgICAgICJxdWVyaWVzIjogW3sicXVlcnkiOiBkYXhfcXVlcnl9XSwKICAgICAgICAic2VyaWFsaXplclNldHRpbmdzIjogeyJpbmNsdWRlTnVsbHMiOiBUcnVlfQogICAgfSkuZW5jb2RlKCJ1dGYtOCIpCiAgICByZXEgPSB1cmxsaWIucmVxdWVzdC5SZXF1ZXN0KHVybCwgZGF0YT1ib2R5LCBoZWFkZXJzPWhlYWRlcnMsIG1ldGhvZD0iUE9TVCIpCiAgICB0cnk6CiAgICAgICAgd2l0aCB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSkgYXMgcmVzcDoKICAgICAgICAgICAgZGF0YSA9IGpzb24ubG9hZHMocmVzcC5yZWFkKCkuZGVjb2RlKCJ1dGYtOCIpKQogICAgICAgICAgICByb3dzID0gZGF0YVsicmVzdWx0cyJdWzBdWyJ0YWJsZXMiXVswXVsicm93cyJdCiAgICAgICAgICAgIHJldHVybiByb3dzWzBdWyJbdmFsXSJdCiAgICBleGNlcHQgKHVybGxpYi5lcnJvci5VUkxFcnJvciwgS2V5RXJyb3IsIEluZGV4RXJyb3IpOgogICAgICAgIHJldHVybiBOb25lCgpkZWYgY29tcGFyZV9tZWFzdXJlcygpOgogICAgcmVzdWx0cyA9IFtdCiAgICBmb3IgbSBpbiBNRUFTVVJFUzoKICAgICAgICBvbGRfdmFsID0gcXVlcnlfZGF0YXNldChEQVRBU0VUX09MRCwgbVsiZGF4Il0pCiAgICAgICAgbmV3X3ZhbCA9IHF1ZXJ5X2RhdGFzZXQoREFUQVNFVF9ORVcsIG1bImRheCJdKQoKICAgICAgICBpZiBvbGRfdmFsIGlzIE5vbmUgb3IgbmV3X3ZhbCBpcyBOb25lOgogICAgICAgICAgICBzdGF0dXMgPSAiRVJST1IiCiAgICAgICAgICAgIGRlbHRhX3N0ciA9ICJOL0EiCiAgICAgICAgZWxzZToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgb2xkX2YgPSBmbG9hdChvbGRfdmFsKQogICAgICAgICAgICAgICAgbmV3X2YgPSBmbG9hdChuZXdfdmFsKQogICAgICAgICAgICAgICAgaWYgb2xkX2YgPT0gMDoKICAgICAgICAgICAgICAgICAgICBkZWx0YSA9IGFicyhuZXdfZikKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgZGVsdGEgPSBhYnMoKG5ld19mIC0gb2xkX2YpIC8gb2xkX2YpCiAgICAgICAgICAgICAgICBzdGF0dXMgPSAiTUlTTUFUQ0giIGlmIGRlbHRhID4gVEhSRVNIT0xEIGVsc2UgIk9LIgogICAgICAgICAgICAgICAgZGVsdGFfc3RyID0gIns6LjQlfSIuZm9ybWF0KGRlbHRhKQogICAgICAgICAgICBleGNlcHQgKFZhbHVlRXJyb3IsIFR5cGVFcnJvcik6CiAgICAgICAgICAgICAgICBkZWx0YV9zdHIgPSAiTi9BIgogICAgICAgICAgICAgICAgc3RhdHVzID0gIkVSUk9SIgoKICAgICAgICByZXN1bHRzLmFwcGVuZCh7CiAgICAgICAgICAgICJtZWFzdXJlIjogbVsibmFtZSJdLAogICAgICAgICAgICAib2xkIjogb2xkX3ZhbCwKICAgICAgICAgICAgIm5ldyI6IG5ld192YWwsCiAgICAgICAgICAgICJkZWx0YSI6IGRlbHRhX3N0ciwKICAgICAgICAgICAgInN0YXR1cyI6IHN0YXR1cwogICAgICAgIH0pCiAgICByZXR1cm4gcmVzdWx0cwoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHByaW50KCI9PT0gREFYIE1lYXN1cmUgQ29tcGFyaXNvbiA9PT1cbiIpCiAgICBwcmludCgiezo8MjB9IHs6PDE1fSB7OjwxNX0gezo8MTJ9IHt9Ii5mb3JtYXQoCiAgICAgICAgIk1lYXN1cmUiLCAiT2xkIE1vZGVsIiwgIk5ldyBNb2RlbCIsICJEZWx0YSIsICJTdGF0dXMiKSkKICAgIHByaW50KCItIiAqIDc1KQoKICAgIHJvd3MgPSBjb21wYXJlX21lYXN1cmVzKCkKICAgIG1pc21hdGNoX2NvdW50ID0gMAogICAgZm9yIHIgaW4gcm93czoKICAgICAgICBwcmludCgiezo8MjB9IHs6PDE1fSB7OjwxNX0gezo8MTJ9IHt9Ii5mb3JtYXQoCiAgICAgICAgICAgIHJbIm1lYXN1cmUiXSwKICAgICAgICAgICAgc3RyKHJbIm9sZCJdKSwKICAgICAgICAgICAgc3RyKHJbIm5ldyJdKSwKICAgICAgICAgICAgclsiZGVsdGEiXSwKICAgICAgICAgICAgclsic3RhdHVzIl0pKQogICAgICAgIGlmIHJbInN0YXR1cyJdID09ICJNSVNNQVRDSCI6CiAgICAgICAgICAgIG1pc21hdGNoX2NvdW50ICs9IDEKCiAgICBpZiBtaXNtYXRjaF9jb3VudCA+IDA6CiAgICAgICAgcHJpbnQoIlxuISB7fSBtZWFzdXJlKHMpIGV4Y2VlZCB7Oi4yJX0gdGhyZXNob2xkIi5mb3JtYXQoCiAgICAgICAgICAgIG1pc21hdGNoX2NvdW50LCBUSFJFU0hPTEQpKQogICAgZWxzZToKICAgICAgICBwcmludCgiXG5BbGwgbWVhc3VyZXMgd2l0aGluIHRocmVzaG9sZCIpIyB5b3VyIGNvZGUgZ29lcyBoZXJl