Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ Notes:
- For CRUD methods that take a record id, pass the GUID string (36-char hyphenated). Parentheses around the GUID are accepted but not required.
- SQL is routed through the Custom API named in `DataverseConfig.sql_api_name` (default: `McpExecuteSqlQuery`).



### Pandas helpers

See `examples/quickstart_pandas.py` for a DataFrame workflow via `PandasODataClient`.
Expand All @@ -152,6 +150,7 @@ VS Code Tasks
## Limitations / Future Work
- No batching, upsert, or association operations yet.
- Minimal retry policy in library (network-error only); examples include additional backoff for transient Dataverse consistency.
- Entity naming conventions in Dataverse (schema/logical/entity set plural & publisher prefix) using the SDK is currently not well-defined

## Contributing

Expand Down
76 changes: 34 additions & 42 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
from pathlib import Path
import os

# Add src to PYTHONPATH for local runs
sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))
Expand All @@ -9,8 +10,15 @@
import traceback
import requests
import time

base_url = 'https://aurorabapenv0f528.crm10.dynamics.com'

if not sys.stdin.isatty():
print("Interactive input required for org URL. Run this script in a TTY.")
sys.exit(1)
entered = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip()
if not entered:
print("No URL entered; exiting.")
sys.exit(1)
base_url = entered.rstrip('/')
client = DataverseClient(base_url=base_url, credential=InteractiveBrowserCredential())

# Small helpers: call logging and step pauses
Expand Down Expand Up @@ -259,50 +267,34 @@ def print_line_summaries(label: str, summaries: list[dict]) -> None:
# 4) Query records via SQL Custom API
print("Query (SQL via Custom API):")
try:
# Try singular logical name first, then plural entity set, with short backoff
import time
plan_call(f"client.query_sql(\"SELECT TOP 2 * FROM {logical} ORDER BY {attr_prefix}_amount DESC\")")
pause("Execute SQL Query")

candidates = [logical]
if entity_set and entity_set != logical:
candidates.append(entity_set)
def _run_query():
log_call(f"client.query_sql(\"SELECT TOP 2 * FROM {logical} ORDER BY {attr_prefix}_amount DESC\")")
return client.query_sql(f"SELECT TOP 2 * FROM {logical} ORDER BY {attr_prefix}_amount DESC")

# Show planned SQL queries before executing
for name in candidates:
plan_call(f"client.query_sql(\"SELECT TOP 2 * FROM {name} ORDER BY {attr_prefix}_amount DESC\")")
pause("Execute SQL Query")
def _retry_if(ex: Exception) -> bool:
msg = str(ex) if ex else ""
return ("Invalid table name" in msg) or ("Invalid object name" in msg)

rows = []
for name in candidates:
def _run_query():
log_call(f"client.query_sql(\"SELECT TOP 2 * FROM {name} ORDER BY {attr_prefix}_amount DESC\")")
return client.query_sql(f"SELECT TOP 2 * FROM {name} ORDER BY {attr_prefix}_amount DESC")
def _retry_if(ex: Exception) -> bool:
msg = str(ex) if ex else ""
return ("Invalid table name" in msg) or ("Invalid object name" in msg)
try:
rows = backoff_retry(_run_query, delays=(0, 2, 5), retry_http_statuses=(), retry_if=_retry_if)
logical_for_ids = logical
id_key = f"{logical_for_ids}id"
ids = [r.get(id_key) for r in rows if isinstance(r, dict) and r.get(id_key)]
print({"entity": name, "rows": len(rows) if isinstance(rows, list) else 0, "ids": ids})
# Print TDS summaries for clarity
tds_summaries = []
for row in rows if isinstance(rows, list) else []:
tds_summaries.append(
{
"id": row.get(id_key),
"code": row.get(code_key),
"count": row.get(count_key),
"amount": row.get(amount_key),
"when": row.get(when_key),
}
)
print_line_summaries("TDS record summaries (top 2 by amount):", tds_summaries)
raise SystemExit
except Exception:
continue
except SystemExit:
pass
rows = backoff_retry(_run_query, delays=(0, 2, 5), retry_http_statuses=(), retry_if=_retry_if)
id_key = f"{logical}id"
ids = [r.get(id_key) for r in rows if isinstance(r, dict) and r.get(id_key)]
print({"entity": logical, "rows": len(rows) if isinstance(rows, list) else 0, "ids": ids})
tds_summaries = []
for row in rows if isinstance(rows, list) else []:
tds_summaries.append(
{
"id": row.get(id_key),
"code": row.get(code_key),
"count": row.get(count_key),
"amount": row.get(amount_key),
"when": row.get(when_key),
}
)
print_line_summaries("TDS record summaries (top 2 by amount):", tds_summaries)
except Exception as e:
print(f"SQL via Custom API failed: {e}")
# 5) Delete record
Expand Down
10 changes: 9 additions & 1 deletion examples/quickstart_pandas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
from pathlib import Path
import os

# Add src to PYTHONPATH for local runs
sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))
Expand All @@ -12,7 +13,14 @@
import time
import pandas as pd

base_url = 'https://aurorabapenv0f528.crm10.dynamics.com'
if not sys.stdin.isatty():
print("Interactive input required for org URL. Run this script in a TTY.")
sys.exit(1)
entered = input("Enter Dataverse org URL (e.g. https://yourorg.crm.dynamics.com): ").strip()
if not entered:
print("No URL entered; exiting.")
sys.exit(1)
base_url = entered.rstrip('/')
client = DataverseClient(base_url=base_url, credential=InteractiveBrowserCredential())
# Use the internal OData client for pandas helpers
PANDAS = PandasODataClient(client._get_odata())
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
azure-identity>=1.17.0
azure-core>=1.30.2
msal>=1.28.0
requests>=2.32.0
pyodbc>=5.1.0
Expand Down