77sys .path .append (str (Path (__file__ ).resolve ().parents [1 ] / "src" ))
88
99from dataverse_sdk import DataverseClient
10+ from enum import IntEnum
1011from azure .identity import InteractiveBrowserCredential
1112import traceback
1213import requests
1516from concurrent .futures import ThreadPoolExecutor , as_completed
1617
1718
18- entered = input ("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): " ).strip ()
19+ # entered = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip()
20+ entered = 'https://aurorabapenv4a0f9.crm10.dynamics.com/'
1921if not entered :
2022 print ("No URL entered; exiting." )
2123 sys .exit (1 )
2224
2325base_url = entered .rstrip ('/' )
24- delete_choice = input ("Delete the new_SampleItem table at end? (Y/n): " ).strip () or "y"
26+ # delete_choice = input("Delete the new_SampleItem table at end? (Y/n): ").strip() or "y"
27+ delete_choice = "n"
2528delete_table_at_end = (str (delete_choice ).lower () in ("y" , "yes" , "true" , "1" ))
2629# Ask once whether to pause between steps during this run
27- pause_choice = input ("Pause between test steps? (y/N): " ).strip () or "n"
30+ # pause_choice = input("Pause between test steps? (y/N): ").strip() or "n"
31+ pause_choice = 'n'
2832pause_between_steps = (str (pause_choice ).lower () in ("y" , "yes" , "true" , "1" ))
2933# Create a credential we can reuse (for DataverseClient)
3034credential = InteractiveBrowserCredential ()
3135client = DataverseClient (base_url = base_url , credential = credential )
3236
37+ # print("Cleanup (Metadata):")
38+ # try:
39+ # info = client.get_table_info("new_SampleItem")
40+ # if info:
41+ # client.delete_table("new_SampleItem")
42+ # print({"table_deleted": True})
43+ # else:
44+ # print({"table_deleted": False, "reason": "not found"})
45+ # except Exception as e:
46+ # print(f"Delete table failed: {e}")
47+ # sys.exit(0)
48+
3349# Small helpers: call logging and step pauses
3450def log_call (call : str ) -> None :
3551 print ({"call" : call })
@@ -44,7 +60,8 @@ def pause(next_step: str) -> None:
4460
4561# Small generic backoff helper used only in this quickstart
4662# Include common transient statuses like 429/5xx to improve resilience.
47- def backoff_retry (op , * , delays = (0 , 2 , 5 , 10 , 20 ), retry_http_statuses = (400 , 403 , 404 , 409 , 412 , 429 , 500 , 502 , 503 , 504 ), retry_if = None ):
63+ # def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403, 404, 409, 412, 429, 500, 502, 503, 504), retry_if=None):
64+ def backoff_retry (op , * , delays = (0 , 1 ), retry_http_statuses = (400 , 403 , 404 , 409 , 412 , 429 , 500 , 502 , 503 , 504 ), retry_if = None ):
4865 last_exc = None
4966 for delay in delays :
5067 if delay :
@@ -65,6 +82,26 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
6582 raise last_exc
6683
6784print ("Ensure custom table exists (Metadata):" )
85+
86+ # Enum demonstrating local option set creation with multilingual labels (example English only here)
87+ class Status (IntEnum ):
88+ Active = 1
89+ Inactive = 2
90+ Archived = 5
91+ __labels__ = {
92+ 1033 : {
93+ "Active" : "Active" ,
94+ "Inactive" : "Inactive" ,
95+ "Archived" : "Archived" ,
96+ },
97+ 1036 : {
98+ "Active" : "Actif" ,
99+ "Inactive" : "Inactif" ,
100+ "Archived" : "Archivé" ,
101+ }
102+ }
103+ # use_french_labels = (input("Use French status labels instead of numeric enum values? (y/N): ").strip().lower() in ("y","yes","1"))
104+ use_french_labels = 'y'
68105table_info = None
69106created_this_run = False
70107
@@ -86,7 +123,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
86123else :
87124 # Create it since it doesn't exist
88125 try :
89- log_call ("client.create_table('new_SampleItem', schema={code,count,amount,when,active})" )
126+ log_call ("client.create_table('new_SampleItem', schema={code,count,amount,when,active,status<enum> })" )
90127 table_info = client .create_table (
91128 "new_SampleItem" ,
92129 {
@@ -95,6 +132,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
95132 "amount" : "decimal" ,
96133 "when" : "datetime" ,
97134 "active" : "bool" ,
135+ "status" : Status , # Enum -> local option set (picklist)
98136 },
99137 )
100138 created_this_run = True if table_info and table_info .get ("columns_created" ) else False
@@ -130,6 +168,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
130168count_key = f"{ attr_prefix } _count"
131169amount_key = f"{ attr_prefix } _amount"
132170when_key = f"{ attr_prefix } _when"
171+ status_key = f"{ attr_prefix } _status" # enum-generated picklist column
133172id_key = f"{ logical } id"
134173
135174def summary_from_record (rec : dict ) -> dict :
@@ -148,6 +187,11 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
148187 f"count={ s .get ('count' )} amount={ s .get ('amount' )} when={ s .get ('when' )} "
149188 )
150189
190+ french_present = 'y'
191+ use_french_labels_effective = (use_french_labels == 'y' and french_present )
192+ if use_french_labels == 'y' and not french_present :
193+ print ({"warning_missing_french_labels" : True , "fallback_to_numeric" : True })
194+
151195# 2) Create a record in the new table
152196print ("Create records (OData) demonstrating single create and bound CreateMultiple (multi):" )
153197
@@ -159,11 +203,22 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
159203 amount_key : 123.45 ,
160204 when_key : "2025-01-01" ,
161205 f"{ attr_prefix } _active" : True ,
206+ status_key : ("Actif" if use_french_labels_effective else Status .Active .value ), # label or int
162207}
163208# Generate multiple payloads
209+ # New behaviour: first half use English labels ("Active"/"Inactive"), second half use French labels ("Actif"/"Inactif")
210+ # (If French labels not actually provisioned, those will fall back to numeric conversion logic.)
164211multi_payloads : list [dict ] = []
212+ total_multi = 4
213+ half_point = total_multi // 2 # for 15 -> 7 (indices >7 use French)
165214base_date = date (2025 , 1 , 2 )
166- for i in range (1 , 16 ):
215+ for i in range (1 , total_multi + 1 ):
216+ use_french_this = french_present and (i > half_point )
217+ if use_french_this :
218+ status_label = "Actif" if (i % 2 ) else "Inactif"
219+ else :
220+ # English label side
221+ status_label = "Active" if (i % 2 ) else "Inactive"
167222 multi_payloads .append (
168223 {
169224 f"{ attr_prefix } _name" : f"Sample { i :02d} " ,
@@ -172,6 +227,7 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
172227 amount_key : round (10.0 * i , 2 ),
173228 when_key : (base_date + timedelta (days = i - 1 )).isoformat (),
174229 f"{ attr_prefix } _active" : True ,
230+ status_key : status_label ,
175231 }
176232 )
177233
@@ -181,12 +237,39 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
181237 # Single create returns list[str] (length 1)
182238 log_call (f"client.create('{ entity_set } ', single_payload)" )
183239 single_ids = backoff_retry (lambda : client .create (entity_set , single_payload ))
184- if not (isinstance (single_ids , list ) and len (single_ids ) == 1 ):
185- raise RuntimeError ("Unexpected single create return shape (expected one-element list)" )
240+ # Defensive normalization: ensure result is list[str]
241+ if isinstance (single_ids , (str , int )):
242+ print ({"debug_single_create_raw_type" : type (single_ids ).__name__ , "raw_value" : str (single_ids )})
243+ single_ids = [str (single_ids )]
244+ elif isinstance (single_ids , list ):
245+ # Coerce any non-str members to str
246+ single_ids = [str (x ) for x in single_ids ]
247+ else :
248+ raise RuntimeError (f"Unexpected single create return type { type (single_ids ).__name__ } " )
249+ if len (single_ids ) != 1 :
250+ raise RuntimeError (f"Unexpected single create list length { len (single_ids )} (expected 1)" )
186251 record_ids .extend (single_ids )
187252
188253 # Multi create returns list[str]
189254 log_call (f"client.create('{ entity_set } ', multi_payloads)" )
255+ # Debug: print full bulk create input plus a concise preview
256+ try :
257+ preview = [
258+ {
259+ "code" : p .get (code_key ),
260+ "when" : p .get (when_key ),
261+ "status" : p .get (status_key ),
262+ }
263+ for p in multi_payloads [:5 ]
264+ ]
265+ print ({
266+ "bulk_create_input_preview" : preview ,
267+ "bulk_create_count" : len (multi_payloads ),
268+ })
269+ # Full payload (small enough at 15 records) for deep debugging
270+ print ({"bulk_create_input_full" : multi_payloads })
271+ except Exception as _ex :
272+ print ({"bulk_create_input_debug_error" : str (_ex )})
190273 multi_ids = backoff_retry (lambda : client .create (entity_set , multi_payloads ))
191274 if isinstance (multi_ids , list ):
192275 record_ids .extend ([mid for mid in multi_ids if isinstance (mid , str )])
@@ -239,11 +322,13 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
239322 f"{ attr_prefix } _amount" : 543.21 ,
240323 f"{ attr_prefix } _when" : "2025-02-02" ,
241324 f"{ attr_prefix } _active" : False ,
325+ status_key : ("Inactif" if use_french_labels else Status .Inactive .value ), # switch enum value or label
242326 }
243327 expected_checks = {
244328 f"{ attr_prefix } _code" : "X002" ,
245329 f"{ attr_prefix } _count" : 99 ,
246330 f"{ attr_prefix } _active" : False ,
331+ status_key : Status .Inactive .value , # verification uses numeric after coercion
247332 }
248333 amount_key = f"{ attr_prefix } _amount"
249334
@@ -280,72 +365,72 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
280365 sys .exit (1 )
281366
282367# 3.6) Bulk update (UpdateMultiple) demo: update count field on up to first 5 remaining records
283- print ("Bulk update (UpdateMultiple) demo:" )
284- try :
285- if len (record_ids ) > 1 :
286- # Prepare a small subset to update (skip the first already updated one)
287- subset = record_ids [1 :6 ]
288- bulk_updates = []
289- for idx , rid in enumerate (subset , start = 1 ):
290- # Simple deterministic changes so user can observe
291- bulk_updates .append ({
292- id_key : rid ,
293- count_key : 100 + idx , # new count values
294- })
295- log_call (f"client.update('{ entity_set } ', <{ len (bulk_updates )} ids>, <patches>)" )
296- # Unified update handles multiple via list of patches (returns None)
297- backoff_retry (lambda : client .update (entity_set , subset , bulk_updates ))
298- print ({"bulk_update_requested" : len (bulk_updates ), "bulk_update_completed" : True })
299- # Verify the updated count values by refetching the subset
300- verification = []
301- # Small delay to reduce risk of any brief replication delay
302- time .sleep (1 )
303- for rid in subset :
304- rec = backoff_retry (lambda rid = rid : client .get (entity_set , rid ))
305- verification .append ({
306- "id" : rid ,
307- "count" : rec .get (count_key ),
308- })
309- print ({"bulk_update_verification" : verification })
310- else :
311- print ({"bulk_update_skipped" : True , "reason" : "not enough records" })
312- except Exception as e :
313- print (f"Bulk update failed: { e } " )
368+ # print("Bulk update (UpdateMultiple) demo:")
369+ # try:
370+ # if len(record_ids) > 1:
371+ # # Prepare a small subset to update (skip the first already updated one)
372+ # subset = record_ids[1:6]
373+ # bulk_updates = []
374+ # for idx, rid in enumerate(subset, start=1):
375+ # # Simple deterministic changes so user can observe
376+ # bulk_updates.append({
377+ # id_key: rid,
378+ # count_key: 100 + idx, # new count values
379+ # })
380+ # log_call(f"client.update('{entity_set}', <{len(bulk_updates)} ids>, <patches>)")
381+ # # Unified update handles multiple via list of patches (returns None)
382+ # backoff_retry(lambda: client.update(entity_set, subset, bulk_updates))
383+ # print({"bulk_update_requested": len(bulk_updates), "bulk_update_completed": True})
384+ # # Verify the updated count values by refetching the subset
385+ # verification = []
386+ # # Small delay to reduce risk of any brief replication delay
387+ # time.sleep(1)
388+ # for rid in subset:
389+ # rec = backoff_retry(lambda rid=rid: client.get(entity_set, rid))
390+ # verification.append({
391+ # "id": rid,
392+ # "count": rec.get(count_key),
393+ # })
394+ # print({"bulk_update_verification": verification})
395+ # else:
396+ # print({"bulk_update_skipped": True, "reason": "not enough records"})
397+ # except Exception as e:
398+ # print(f"Bulk update failed: {e}")
314399
315400# 4) Query records via SQL (?sql parameter))
316- print ("Query (SQL via ?sql query parameter):" )
317- try :
318- import time
319- pause ("Execute SQL Query" )
320-
321- def _run_query ():
322- cols = f"{ id_key } , { code_key } , { amount_key } , { when_key } "
323- query = f"SELECT TOP 2 { cols } FROM { logical } ORDER BY { attr_prefix } _amount DESC"
324- log_call (f"client.query_sql(\" { query } \" ) (Web API ?sql=)" )
325- return client .query_sql (query )
326-
327- def _retry_if (ex : Exception ) -> bool :
328- msg = str (ex ) if ex else ""
329- return ("Invalid table name" in msg ) or ("Invalid object name" in msg )
330-
331- rows = backoff_retry (_run_query , delays = (0 , 2 , 5 ), retry_http_statuses = (), retry_if = _retry_if )
332- id_key = f"{ logical } id"
333- ids = [r .get (id_key ) for r in rows if isinstance (r , dict ) and r .get (id_key )]
334- print ({"entity" : logical , "rows" : len (rows ) if isinstance (rows , list ) else 0 , "ids" : ids })
335- record_summaries = []
336- for row in rows if isinstance (rows , list ) else []:
337- record_summaries .append (
338- {
339- "id" : row .get (id_key ),
340- "code" : row .get (code_key ),
341- "count" : row .get (count_key ),
342- "amount" : row .get (amount_key ),
343- "when" : row .get (when_key ),
344- }
345- )
346- print_line_summaries ("SQL record summaries (top 2 by amount):" , record_summaries )
347- except Exception as e :
348- print (f"SQL query failed: { e } " )
401+ # print("Query (SQL via ?sql query parameter):")
402+ # try:
403+ # import time
404+ # pause("Execute SQL Query")
405+
406+ # def _run_query():
407+ # cols = f"{id_key}, {code_key}, {amount_key}, {when_key}"
408+ # query = f"SELECT TOP 2 {cols} FROM {logical} ORDER BY {attr_prefix}_amount DESC"
409+ # log_call(f"client.query_sql(\"{query}\") (Web API ?sql=)")
410+ # return client.query_sql(query)
411+
412+ # def _retry_if(ex: Exception) -> bool:
413+ # msg = str(ex) if ex else ""
414+ # return ("Invalid table name" in msg) or ("Invalid object name" in msg)
415+
416+ # rows = backoff_retry(_run_query, delays=(0, 2, 5), retry_http_statuses=(), retry_if=_retry_if)
417+ # id_key = f"{logical}id"
418+ # ids = [r.get(id_key) for r in rows if isinstance(r, dict) and r.get(id_key)]
419+ # print({"entity": logical, "rows": len(rows) if isinstance(rows, list) else 0, "ids": ids})
420+ # record_summaries = []
421+ # for row in rows if isinstance(rows, list) else []:
422+ # record_summaries.append(
423+ # {
424+ # "id": row.get(id_key),
425+ # "code": row.get(code_key),
426+ # "count": row.get(count_key),
427+ # "amount": row.get(amount_key),
428+ # "when": row.get(when_key),
429+ # }
430+ # )
431+ # print_line_summaries("SQL record summaries (top 2 by amount):", record_summaries)
432+ # except Exception as e:
433+ # print(f"SQL query failed: {e}")
349434
350435# Pause between SQL query and retrieve-multiple demos
351436pause ("Retrieve multiple (OData paging demos)" )
@@ -357,6 +442,8 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])
357442 total = 0
358443 page_index = 0
359444 _select = [id_key , code_key , amount_key , when_key ]
445+ # Include status column in select so we can show picklist numeric value
446+ _select .append (status_key )
360447 _orderby = [f"{ code_key } asc" ]
361448 for page in client .get_multiple (
362449 entity_set ,
@@ -373,7 +460,13 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])
373460 "page" : page_index ,
374461 "page_size" : len (page ),
375462 "sample" : [
376- {"id" : r .get (id_key ), "code" : r .get (code_key ), "amount" : r .get (amount_key ), "when" : r .get (when_key )}
463+ {
464+ "id" : r .get (id_key ),
465+ "code" : r .get (code_key ),
466+ "amount" : r .get (amount_key ),
467+ "when" : r .get (when_key ),
468+ "status" : r .get (status_key ),
469+ }
377470 for r in page [:5 ]
378471 ],
379472 })
0 commit comments