Skip to content

Commit 835976f

Browse files
tpellissierclaude
andcommitted
Add alternate keys + upsert E2E example
Demonstrates full workflow: create table, create alternate key, wait for index, upsert records, verify, clean up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1799439 commit 835976f

1 file changed

Lines changed: 212 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT license.
4+
5+
"""
6+
PowerPlatform Dataverse Client - Alternate Keys & Upsert Example
7+
8+
Demonstrates the full workflow of creating alternate keys and using
9+
them for upsert operations:
10+
1. Create a custom table with columns
11+
2. Define an alternate key on a column
12+
3. Wait for the key index to become Active
13+
4. Upsert records using the alternate key
14+
5. Verify records were created/updated correctly
15+
6. Clean up
16+
17+
Prerequisites:
18+
pip install PowerPlatform-Dataverse-Client
19+
pip install azure-identity
20+
"""
21+
22+
import sys
23+
import time
24+
25+
from PowerPlatform.Dataverse.client import DataverseClient
26+
from PowerPlatform.Dataverse.models.upsert import UpsertItem
27+
from azure.identity import InteractiveBrowserCredential # type: ignore
28+
29+
# --- Config ---
30+
TABLE_NAME = "new_AltKeyDemo"
31+
KEY_COLUMN = "new_externalid"
32+
KEY_NAME = "new_ExternalIdKey"
33+
BACKOFF_DELAYS = (0, 3, 10, 20, 35)
34+
35+
36+
# --- Helpers ---
37+
def backoff(op, *, delays=BACKOFF_DELAYS):
38+
"""Retry *op* with exponential-ish backoff on any exception."""
39+
last = None
40+
total_delay = 0
41+
attempts = 0
42+
for d in delays:
43+
if d:
44+
time.sleep(d)
45+
total_delay += d
46+
attempts += 1
47+
try:
48+
result = op()
49+
if attempts > 1:
50+
retry_count = attempts - 1
51+
print(f" [INFO] Backoff succeeded after {retry_count} retry(s); " f"waited {total_delay}s total.")
52+
return result
53+
except Exception as ex: # noqa: BLE001
54+
last = ex
55+
continue
56+
if last:
57+
if attempts:
58+
retry_count = max(attempts - 1, 0)
59+
print(f" [WARN] Backoff exhausted after {retry_count} retry(s); " f"waited {total_delay}s total.")
60+
raise last
61+
62+
63+
def wait_for_key_active(client, table, key_name, max_wait=120):
64+
"""Poll get_alternate_keys until the key status is Active."""
65+
start = time.time()
66+
while time.time() - start < max_wait:
67+
keys = client.tables.get_alternate_keys(table)
68+
for k in keys:
69+
if k.schema_name == key_name:
70+
print(f" Key status: {k.status}")
71+
if k.status == "Active":
72+
return k
73+
if k.status == "Failed":
74+
raise RuntimeError(f"Alternate key index failed: {k.schema_name}")
75+
time.sleep(5)
76+
raise TimeoutError(f"Key {key_name} did not become Active within {max_wait}s")
77+
78+
79+
# --- Main ---
80+
def main():
81+
"""Run the alternate-keys & upsert E2E walkthrough."""
82+
print("PowerPlatform Dataverse Client - Alternate Keys & Upsert Example")
83+
print("=" * 70)
84+
print("This script demonstrates:")
85+
print(" - Creating a custom table with columns")
86+
print(" - Defining an alternate key on a column")
87+
print(" - Waiting for the key index to become Active")
88+
print(" - Upserting records via alternate key (create + update)")
89+
print(" - Verifying records and listing keys")
90+
print(" - Cleaning up (delete key, delete table)")
91+
print("=" * 70)
92+
93+
entered = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip()
94+
if not entered:
95+
print("No URL entered; exiting.")
96+
sys.exit(1)
97+
98+
base_url = entered.rstrip("/")
99+
credential = InteractiveBrowserCredential()
100+
client = DataverseClient(base_url, credential)
101+
102+
# ------------------------------------------------------------------
103+
# Step 1: Create table
104+
# ------------------------------------------------------------------
105+
print("\n1. Creating table...")
106+
table_info = backoff(
107+
lambda: client.tables.create(
108+
TABLE_NAME,
109+
columns={
110+
KEY_COLUMN: "string",
111+
"new_ProductName": "string",
112+
"new_Price": "decimal",
113+
},
114+
)
115+
)
116+
print(f" Created: {table_info.get('table_schema_name', TABLE_NAME)}")
117+
118+
time.sleep(10) # Wait for metadata propagation
119+
120+
# ------------------------------------------------------------------
121+
# Step 2: Create alternate key
122+
# ------------------------------------------------------------------
123+
print("\n2. Creating alternate key...")
124+
key_info = backoff(lambda: client.tables.create_alternate_key(TABLE_NAME, KEY_NAME, [KEY_COLUMN.lower()]))
125+
print(f" Key created: {key_info.schema_name} (id={key_info.metadata_id})")
126+
127+
# ------------------------------------------------------------------
128+
# Step 3: Wait for key to become Active
129+
# ------------------------------------------------------------------
130+
print("\n3. Waiting for key index to become Active...")
131+
active_key = wait_for_key_active(client, TABLE_NAME, KEY_NAME)
132+
print(f" Key is Active: {active_key.schema_name}")
133+
134+
# ------------------------------------------------------------------
135+
# Step 4: Upsert records (creates new)
136+
# ------------------------------------------------------------------
137+
print("\n4. Upserting records (initial create)...")
138+
client.records.upsert(
139+
TABLE_NAME,
140+
[
141+
UpsertItem(
142+
alternate_key={KEY_COLUMN.lower(): "EXT-001"},
143+
record={"new_productname": "Widget A", "new_price": 9.99},
144+
),
145+
UpsertItem(
146+
alternate_key={KEY_COLUMN.lower(): "EXT-002"},
147+
record={"new_productname": "Widget B", "new_price": 19.99},
148+
),
149+
],
150+
)
151+
print(" Upserted 2 records")
152+
153+
# ------------------------------------------------------------------
154+
# Step 5: Upsert again (updates existing)
155+
# ------------------------------------------------------------------
156+
print("\n5. Upserting records (update existing)...")
157+
client.records.upsert(
158+
TABLE_NAME,
159+
[
160+
UpsertItem(
161+
alternate_key={KEY_COLUMN.lower(): "EXT-001"},
162+
record={"new_productname": "Widget A v2", "new_price": 12.99},
163+
),
164+
],
165+
)
166+
print(" Updated EXT-001")
167+
168+
# ------------------------------------------------------------------
169+
# Step 6: Verify
170+
# ------------------------------------------------------------------
171+
print("\n6. Verifying records...")
172+
for page in client.records.get(
173+
TABLE_NAME,
174+
select=["new_productname", "new_price", KEY_COLUMN.lower()],
175+
):
176+
for record in page:
177+
ext_id = record.get(KEY_COLUMN.lower(), "?")
178+
name = record.get("new_productname", "?")
179+
price = record.get("new_price", "?")
180+
print(f" {ext_id}: {name} @ ${price}")
181+
182+
# ------------------------------------------------------------------
183+
# Step 7: List alternate keys
184+
# ------------------------------------------------------------------
185+
print("\n7. Listing alternate keys...")
186+
keys = client.tables.get_alternate_keys(TABLE_NAME)
187+
for k in keys:
188+
print(f" {k.schema_name}: columns={k.key_attributes}, status={k.status}")
189+
190+
# ------------------------------------------------------------------
191+
# Step 8: Cleanup
192+
# ------------------------------------------------------------------
193+
cleanup = input("\n8. Delete table and cleanup? (Y/n): ").strip() or "y"
194+
if cleanup.lower() in ("y", "yes"):
195+
try:
196+
# Delete alternate key first
197+
for k in keys:
198+
client.tables.delete_alternate_key(TABLE_NAME, k.metadata_id)
199+
print(f" Deleted key: {k.schema_name}")
200+
time.sleep(5)
201+
backoff(lambda: client.tables.delete(TABLE_NAME))
202+
print(f" Deleted table: {TABLE_NAME}")
203+
except Exception as e: # noqa: BLE001
204+
print(f" Cleanup error: {e}")
205+
else:
206+
print(" Table kept for inspection.")
207+
208+
print("\nDone.")
209+
210+
211+
if __name__ == "__main__":
212+
main()

0 commit comments

Comments
 (0)