|
| 1 | +# Copyright (c) Microsoft Corporation. |
| 2 | +# Licensed under the MIT license. |
| 3 | + |
| 4 | +""" |
| 5 | +PowerPlatform Dataverse Client - DataFrame Operations Walkthrough |
| 6 | +
|
| 7 | +This example demonstrates how to use the pandas DataFrame extension methods |
| 8 | +for CRUD operations with Microsoft Dataverse. |
| 9 | +
|
| 10 | +Prerequisites: |
| 11 | + pip install PowerPlatform-Dataverse-Client |
| 12 | + pip install azure-identity |
| 13 | +""" |
| 14 | + |
| 15 | +import sys |
| 16 | +import uuid |
| 17 | + |
| 18 | +import pandas as pd |
| 19 | +from azure.identity import InteractiveBrowserCredential |
| 20 | + |
| 21 | +from PowerPlatform.Dataverse.client import DataverseClient |
| 22 | + |
| 23 | + |
| 24 | +def main(): |
| 25 | + # -- Setup & Authentication ------------------------------------ |
| 26 | + base_url = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip() |
| 27 | + if not base_url: |
| 28 | + print("[ERR] No URL entered; exiting.") |
| 29 | + sys.exit(1) |
| 30 | + base_url = base_url.rstrip("/") |
| 31 | + |
| 32 | + print("[INFO] Authenticating via browser...") |
| 33 | + credential = InteractiveBrowserCredential() |
| 34 | + |
| 35 | + with DataverseClient(base_url, credential) as client: |
| 36 | + _run_walkthrough(client) |
| 37 | + |
| 38 | + |
| 39 | +def _run_walkthrough(client): |
| 40 | + table = input("Enter table schema name to use [default: account]: ").strip() or "account" |
| 41 | + print(f"[INFO] Using table: {table}") |
| 42 | + |
| 43 | + # Unique tag to isolate test records from existing data |
| 44 | + tag = uuid.uuid4().hex[:8] |
| 45 | + test_filter = f"contains(name,'{tag}')" |
| 46 | + print(f"[INFO] Using tag '{tag}' to identify test records") |
| 47 | + |
| 48 | + select_cols = ["name", "telephone1", "websiteurl", "lastonholdtime"] |
| 49 | + |
| 50 | + # -- 1. Create records from a DataFrame ------------------------ |
| 51 | + print("\n" + "-" * 60) |
| 52 | + print("1. Create records from a DataFrame") |
| 53 | + print("-" * 60) |
| 54 | + |
| 55 | + new_accounts = pd.DataFrame( |
| 56 | + [ |
| 57 | + { |
| 58 | + "name": f"Contoso_{tag}", |
| 59 | + "telephone1": "555-0100", |
| 60 | + "websiteurl": "https://contoso.com", |
| 61 | + "lastonholdtime": pd.Timestamp("2024-06-15 10:30:00"), |
| 62 | + }, |
| 63 | + {"name": f"Fabrikam_{tag}", "telephone1": "555-0200", "websiteurl": None, "lastonholdtime": None}, |
| 64 | + { |
| 65 | + "name": f"Northwind_{tag}", |
| 66 | + "telephone1": None, |
| 67 | + "websiteurl": "https://northwind.com", |
| 68 | + "lastonholdtime": pd.Timestamp("2024-12-01 08:00:00"), |
| 69 | + }, |
| 70 | + ] |
| 71 | + ) |
| 72 | + print(f" Input DataFrame:\n{new_accounts.to_string(index=False)}\n") |
| 73 | + |
| 74 | + # create_dataframe returns a Series of GUIDs aligned with the input rows |
| 75 | + new_accounts["accountid"] = client.dataframe.create(table, new_accounts) |
| 76 | + print(f"[OK] Created {len(new_accounts)} records") |
| 77 | + print(f" IDs: {new_accounts['accountid'].tolist()}") |
| 78 | + |
| 79 | + # -- 2. Query records as a DataFrame ------------------------- |
| 80 | + print("\n" + "-" * 60) |
| 81 | + print("2. Query records as a DataFrame") |
| 82 | + print("-" * 60) |
| 83 | + |
| 84 | + df_all = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 85 | + print(f"[OK] Got {len(df_all)} records in one DataFrame") |
| 86 | + print(f" Columns: {list(df_all.columns)}") |
| 87 | + print(f"{df_all.to_string(index=False)}") |
| 88 | + |
| 89 | + # -- 3. Limit results with top ------------------------------ |
| 90 | + print("\n" + "-" * 60) |
| 91 | + print("3. Limit results with top") |
| 92 | + print("-" * 60) |
| 93 | + |
| 94 | + df_top2 = client.dataframe.get(table, select=select_cols, filter=test_filter, top=2) |
| 95 | + print(f"[OK] Got {len(df_top2)} records with top=2") |
| 96 | + print(f"{df_top2.to_string(index=False)}") |
| 97 | + |
| 98 | + # -- 4. Fetch a single record by ID ---------------------------- |
| 99 | + print("\n" + "-" * 60) |
| 100 | + print("4. Fetch a single record by ID") |
| 101 | + print("-" * 60) |
| 102 | + |
| 103 | + first_id = new_accounts["accountid"].iloc[0] |
| 104 | + print(f" Fetching record {first_id}...") |
| 105 | + single = client.dataframe.get(table, record_id=first_id, select=select_cols) |
| 106 | + print(f"[OK] Single record DataFrame:\n{single.to_string(index=False)}") |
| 107 | + |
| 108 | + # -- 5. Update records from a DataFrame ------------------------ |
| 109 | + print("\n" + "-" * 60) |
| 110 | + print("5. Update records with different values per row") |
| 111 | + print("-" * 60) |
| 112 | + |
| 113 | + new_accounts["telephone1"] = ["555-1100", "555-1200", "555-1300"] |
| 114 | + print(f" New telephone numbers: {new_accounts['telephone1'].tolist()}") |
| 115 | + client.dataframe.update(table, new_accounts[["accountid", "telephone1"]], id_column="accountid") |
| 116 | + print("[OK] Updated 3 records") |
| 117 | + |
| 118 | + # Verify the updates |
| 119 | + verified = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 120 | + print(f" Verified:\n{verified.to_string(index=False)}") |
| 121 | + |
| 122 | + # -- 6. Broadcast update (same value to all records) ----------- |
| 123 | + print("\n" + "-" * 60) |
| 124 | + print("6. Broadcast update (same value to all records)") |
| 125 | + print("-" * 60) |
| 126 | + |
| 127 | + broadcast_df = new_accounts[["accountid"]].copy() |
| 128 | + broadcast_df["websiteurl"] = "https://updated.example.com" |
| 129 | + print(f" Setting websiteurl to 'https://updated.example.com' for all {len(broadcast_df)} records") |
| 130 | + client.dataframe.update(table, broadcast_df, id_column="accountid") |
| 131 | + print("[OK] Broadcast update complete") |
| 132 | + |
| 133 | + # Verify all records have the same websiteurl |
| 134 | + verified = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 135 | + print(f" Verified:\n{verified.to_string(index=False)}") |
| 136 | + |
| 137 | + # Default: NaN/None fields are skipped (not overridden on server) |
| 138 | + print("\n Updating with NaN values (default: clear_nulls=False, fields should stay unchanged)...") |
| 139 | + sparse_df = pd.DataFrame( |
| 140 | + [ |
| 141 | + {"accountid": new_accounts["accountid"].iloc[0], "telephone1": "555-9999", "websiteurl": None}, |
| 142 | + ] |
| 143 | + ) |
| 144 | + client.dataframe.update(table, sparse_df, id_column="accountid") |
| 145 | + verified = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 146 | + print(f" Verified (Contoso telephone1 updated, websiteurl unchanged):\n{verified.to_string(index=False)}") |
| 147 | + |
| 148 | + # Opt-in: clear_nulls=True sends None as null to clear the field |
| 149 | + print("\n Clearing websiteurl for Contoso with clear_nulls=True...") |
| 150 | + clear_df = pd.DataFrame([{"accountid": new_accounts["accountid"].iloc[0], "websiteurl": None}]) |
| 151 | + client.dataframe.update(table, clear_df, id_column="accountid", clear_nulls=True) |
| 152 | + verified = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 153 | + print(f" Verified (Contoso websiteurl should be empty):\n{verified.to_string(index=False)}") |
| 154 | + |
| 155 | + # -- 7. Delete records by passing a Series of GUIDs ------------ |
| 156 | + print("\n" + "-" * 60) |
| 157 | + print("7. Delete records by passing a Series of GUIDs") |
| 158 | + print("-" * 60) |
| 159 | + |
| 160 | + print(f" Deleting {len(new_accounts)} records...") |
| 161 | + client.dataframe.delete(table, new_accounts["accountid"], use_bulk_delete=False) |
| 162 | + print(f"[OK] Deleted {len(new_accounts)} records") |
| 163 | + |
| 164 | + # Verify deletions - filter for our tagged records should return 0 |
| 165 | + remaining = client.dataframe.get(table, select=select_cols, filter=test_filter) |
| 166 | + print(f" Verified: {len(remaining)} test records remaining (expected 0)") |
| 167 | + |
| 168 | + print("\n" + "=" * 60) |
| 169 | + print("[OK] DataFrame operations walkthrough complete!") |
| 170 | + print("=" * 60) |
| 171 | + |
| 172 | + |
| 173 | +if __name__ == "__main__": |
| 174 | + main() |
0 commit comments