Skip to content

Commit 5f17919

Browse files
author
Max Wang
committed
NOT COMPLETED: push for demo
1 parent 8fcba62 commit 5f17919

3 files changed

Lines changed: 422 additions & 78 deletions

File tree

examples/quickstart.py

Lines changed: 166 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))
88

99
from dataverse_sdk import DataverseClient
10+
from enum import IntEnum
1011
from azure.identity import InteractiveBrowserCredential
1112
import traceback
1213
import requests
@@ -15,21 +16,36 @@
1516
from 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/'
1921
if not entered:
2022
print("No URL entered; exiting.")
2123
sys.exit(1)
2224

2325
base_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"
2528
delete_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'
2832
pause_between_steps = (str(pause_choice).lower() in ("y", "yes", "true", "1"))
2933
# Create a credential we can reuse (for DataverseClient)
3034
credential = InteractiveBrowserCredential()
3135
client = 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
3450
def 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

6784
print("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'
68105
table_info = None
69106
created_this_run = False
70107

@@ -86,7 +123,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
86123
else:
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
130168
count_key = f"{attr_prefix}_count"
131169
amount_key = f"{attr_prefix}_amount"
132170
when_key = f"{attr_prefix}_when"
171+
status_key = f"{attr_prefix}_status" # enum-generated picklist column
133172
id_key = f"{logical}id"
134173

135174
def 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
152196
print("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.)
164211
multi_payloads: list[dict] = []
212+
total_multi = 4
213+
half_point = total_multi // 2 # for 15 -> 7 (indices >7 use French)
165214
base_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
351436
pause("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
})

src/dataverse_sdk/client.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,19 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]:
206206
"""
207207
return self._get_odata()._get_table_info(tablename)
208208

209-
def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]:
209+
def create_table(self, tablename: str, schema: Dict[str, Any]) -> Dict[str, Any]:
210210
"""Create a simple custom table.
211211
212212
Parameters
213213
----------
214214
tablename : str
215215
Friendly name (``"SampleItem"``) or a full schema name (``"new_SampleItem"``).
216-
schema : dict[str, str]
216+
schema : dict[str, Any]
217217
Column definitions mapping logical names (without prefix) to types.
218-
Supported: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
218+
Supported:
219+
- Primitive type tokens: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``
220+
- Enum subclass (IntEnum preferred): generates a local option set with each member value.
221+
Optional multilingual labels via ``__labels__ = {1033: {"Active": "Active"}, 1036: {"Active": "Actif"}}``
219222
220223
Returns
221224
-------

0 commit comments

Comments
 (0)