Skip to content

Commit 36c1d9b

Browse files
author
Max Wang
committed
add CreateInstantEntities API
1 parent 94e84f0 commit 36c1d9b

4 files changed

Lines changed: 354 additions & 21 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Direct TDS via ODBC is not used; SQL reads are executed via the Custom API over
3939

4040
- For Web API (OData), tokens target your Dataverse org URL scope: https://yourorg.crm.dynamics.com/.default. The SDK requests this scope from the provided TokenCredential.
4141
- For complete functionalities, please use one of the PREPROD BAP environments, otherwise McpExecuteSqlQuery might not work.
42+
- For CreateInstantEntities call, it's a prerequisite to import solution https://microsoft-my.sharepoint.com/:u:/p/cdietric/EXuJB0ZywshPuVAc54b2HxUBpnlv9jjQl47QCrx-VhSErA?e=4cdYm0
4243

4344
### Configuration (DataverseConfig)
4445

@@ -69,6 +70,7 @@ The quickstart demonstrates:
6970
- Bulk create (CreateMultiple) to insert many records in one call
7071
- Retrieve multiple with paging (contrasting `$top` vs `page_size`)
7172
- Executing a read-only SQL query
73+
- Use CreateInstantEntities API to quickly create entities
7274

7375
## Examples
7476

@@ -218,6 +220,26 @@ info = client.create_table(
218220
},
219221
)
220222

223+
# Alternatively create a custom table with use_instant option
224+
# Only text type column is supported and lookups and display_name are required inputs
225+
# info = client.create_table(
226+
# "new_SampleItemInstant",
227+
# {
228+
# "code": "text",
229+
# "count": "text",
230+
# },
231+
# use_instant=True,
232+
# display_name="Sample Item",
233+
# lookups=[
234+
# {
235+
# "AttributeName": "new_Account",
236+
# "AttributeDisplayName": "Account (Demo Lookup)",
237+
# "ReferencedEntityName": "account",
238+
# "RelationshipName": "new_newSampleItem_account",
239+
# }
240+
# ],
241+
# )
242+
221243
entity_set = info["entity_set_name"] # e.g., "new_sampleitems"
222244
logical = info["entity_logical_name"] # e.g., "new_sampleitem"
223245

examples/quickstart.py

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
# Ask once whether to pause between steps during this run
2727
pause_choice = input("Pause between test steps? (y/N): ").strip() or "n"
2828
pause_between_steps = (str(pause_choice).lower() in ("y", "yes", "true", "1"))
29+
instant_create_choice = input("Run instant create demo? (y/N): ").strip() or "n"
30+
run_instant_create = (str(instant_create_choice).lower() in ("y", "yes", "true", "1"))
2931
# Create a credential we can reuse (for DataverseClient)
3032
credential = InteractiveBrowserCredential()
3133
client = DataverseClient(base_url=base_url, credential=credential)
@@ -42,6 +44,20 @@ def pause(next_step: str) -> None:
4244
# If stdin is not available, just proceed
4345
pass
4446

47+
# Helper: delete a table if it exists
48+
def delete_table_if_exists(table_name: str) -> None:
49+
try:
50+
log_call(f"client.get_table_info('{table_name}')")
51+
info = client.get_table_info(table_name)
52+
if info:
53+
log_call(f"client.delete_table('{table_name}')")
54+
client.delete_table(table_name)
55+
print({"table_deleted": True})
56+
else:
57+
print({"table_deleted": False, "reason": "not found"})
58+
except Exception as e:
59+
print({f"Delete table failed": str(e)})
60+
4561
# Small generic backoff helper used only in this quickstart
4662
# Include common transient statuses like 429/5xx to improve resilience.
4763
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):
@@ -68,6 +84,11 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
6884
table_info = None
6985
created_this_run = False
7086

87+
# Timing metrics for comparison (seconds)
88+
instant_create_seconds: float | None = None
89+
standard_create_seconds: float | None = None
90+
warm_up_seconds: float | None = None
91+
7192
# Check for existing table using list_tables
7293
log_call("client.list_tables()")
7394
tables = client.list_tables()
@@ -87,6 +108,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
87108
# Create it since it doesn't exist
88109
try:
89110
log_call("client.create_table('new_SampleItem', schema={code,count,amount,when,active})")
111+
_t0_standard = time.perf_counter()
90112
table_info = client.create_table(
91113
"new_SampleItem",
92114
{
@@ -97,6 +119,7 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
97119
"active": "bool",
98120
},
99121
)
122+
standard_create_seconds = time.perf_counter() - _t0_standard
100123
created_this_run = True if table_info and table_info.get("columns_created") else False
101124
print({
102125
"table": table_info.get("entity_schema") if table_info else None,
@@ -124,6 +147,19 @@ def backoff_retry(op, *, delays=(0, 2, 5, 10, 20), retry_http_statuses=(400, 403
124147
entity_set = table_info.get("entity_set_name")
125148
logical = table_info.get("entity_logical_name") or entity_set.rstrip("s")
126149

150+
if run_instant_create:
151+
# call early to warm up for instant create
152+
log_call("client.warm_up_instant_create()")
153+
try:
154+
_t0_warm = time.perf_counter()
155+
client.warm_up_instant_create()
156+
warm_up_seconds = time.perf_counter() - _t0_warm
157+
print({"warm_up_for_instant_create": True, "warm_up_seconds": warm_up_seconds})
158+
except Exception as warm_ex:
159+
print({"warm_up_for_instant_create_error": str(warm_ex)})
160+
# Abort instant demo if warm-up fails
161+
sys.exit(1)
162+
127163
# Derive attribute logical name prefix from the entity logical name (segment before first underscore)
128164
attr_prefix = logical.split("_", 1)[0] if "_" in logical else logical
129165
code_key = f"{attr_prefix}_code"
@@ -410,21 +446,77 @@ def _del_one(rid: str) -> tuple[str, bool, str | None]:
410446
except Exception as e:
411447
print(f"Delete failed: {e}")
412448

449+
# 6) (Optional) Instant create path demo
450+
if not run_instant_create:
451+
print("Skipping instant create demo as per user choice.")
452+
else:
453+
pause("Next: instant create demo")
454+
455+
print("Instant create demo")
456+
print("Delete Instant table first if exists")
457+
# Delete instant table first
458+
delete_table_if_exists("new_SampleItemInstant")
459+
460+
# Create Instant
461+
log_call("client.create_table('new_SampleItemInstant', instant_create)")
462+
instant_schema = {
463+
"code": "text",
464+
"count": "text",
465+
}
466+
# Demo dummy lookup definition (must supply at least one for instant path)
467+
instant_lookups = [
468+
{
469+
"AttributeName": "new_Account",
470+
"AttributeDisplayName": "Account (Demo Lookup)",
471+
"ReferencedEntityName": "account",
472+
"RelationshipName": "new_newSampleItem_account",
473+
}
474+
]
475+
try:
476+
_t0_instant = time.perf_counter()
477+
_table_instant = client.create_table(
478+
"new_SampleItemInstant",
479+
instant_schema,
480+
use_instant=True,
481+
display_name="Sample Item",
482+
lookups=instant_lookups,
483+
)
484+
instant_create_seconds = time.perf_counter() - _t0_instant
485+
table_info = _table_instant
486+
logical = table_info.get("entity_logical_name") if isinstance(table_info, dict) else None
487+
print(table_info)
488+
except Exception as instant_ex:
489+
print({"instant_create_error": str(instant_ex)})
490+
sys.exit(1)
491+
492+
# Timing comparison summary for table creation
493+
_standard_create_ran = standard_create_seconds is not None
494+
_instant_create_ran = instant_create_seconds is not None
495+
print({
496+
"table_creation_timing_compare": {
497+
"warm_up_seconds": warm_up_seconds,
498+
"instant_seconds": instant_create_seconds,
499+
"warm_up+instant_seconds": warm_up_seconds + instant_create_seconds,
500+
"standard_seconds": standard_create_seconds if _standard_create_ran else "standard table pre-existed; omitted",
501+
"delta_standard_minus_instant": (
502+
(standard_create_seconds - instant_create_seconds)
503+
if (_standard_create_ran and _instant_create_ran)
504+
else None
505+
),
506+
}
507+
})
508+
509+
413510
pause("Next: Cleanup table")
414511

415-
# 6) Cleanup: delete the custom table if it exists
512+
# 7) Cleanup: delete the custom table if it exists
416513
print("Cleanup (Metadata):")
417514
if delete_table_at_end:
418-
try:
419-
log_call("client.get_table_info('new_SampleItem')")
420-
info = client.get_table_info("new_SampleItem")
421-
if info:
422-
log_call("client.delete_table('new_SampleItem')")
423-
client.delete_table("new_SampleItem")
424-
print({"table_deleted": True})
425-
else:
426-
print({"table_deleted": False, "reason": "not found"})
427-
except Exception as e:
428-
print(f"Delete table failed: {e}")
515+
delete_table_if_exists("new_SampleItem")
429516
else:
430517
print({"table_deleted": False, "reason": "user opted to keep table"})
518+
519+
# Put instant table delete at the end to avoid metadata cache issues when deletion immediately follows creation
520+
if run_instant_create:
521+
print("Cleanup instant (Metadata):")
522+
delete_table_if_exists("new_SampleItemInstant")

src/dataverse_sdk/client.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,24 +198,52 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]:
198198
"""
199199
return self._get_odata().get_table_info(tablename)
200200

201-
def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]:
202-
"""Create a simple custom table.
201+
def create_table(
202+
self,
203+
tablename: str,
204+
schema: Dict[str, str],
205+
use_instant: bool = False,
206+
display_name: Optional[str] = None,
207+
lookups: Optional[List[Dict[str, str]]] = None,
208+
) -> Dict[str, Any]:
209+
"""Create a custom table (standard or instant path).
210+
211+
Standard path (default): uses supported metadata APIs and allows a variety of column types
212+
(``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``).
213+
214+
Instant path (``use_instant=True``): attempts creation via the CreateInstantEntities API.
215+
This path currently only supports ``text`` columns and requires at least one lookup
216+
relationship
203217
204218
Parameters
205219
----------
206220
tablename : str
207221
Friendly name (``"SampleItem"``) or a full schema name (``"new_SampleItem"``).
208222
schema : dict[str, str]
209223
Column definitions mapping logical names (without prefix) to types.
210-
Supported: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
224+
Supported for standard path: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
225+
For instant path you must supply only text columns (``text``).
226+
use_instant : bool, default False
227+
If True, use the instant entity creation path. Must call warm_up_instant_create before using it
228+
display_name : str | None
229+
Required when ``use_instant`` is True. Singular display label (collection name pluralized with an ``s``).
230+
lookups : list[dict] | None
231+
Required when ``use_instant`` is True. Each item must include:
232+
``AttributeName``, ``AttributeDisplayName``, ``ReferencedEntityName``, ``RelationshipName``.
211233
212234
Returns
213235
-------
214236
dict
215237
Metadata summary including ``entity_schema``, ``entity_set_name``,
216238
``entity_logical_name``, ``metadata_id``, and ``columns_created``.
217239
"""
218-
return self._get_odata().create_table(tablename, schema)
240+
return self._get_odata().create_table(
241+
tablename,
242+
schema,
243+
use_instant=use_instant,
244+
display_name=display_name,
245+
lookups=lookups,
246+
)
219247

220248
def delete_table(self, tablename: str) -> None:
221249
"""Delete a custom table by name.
@@ -237,6 +265,15 @@ def list_tables(self) -> list[str]:
237265
"""
238266
return self._get_odata().list_tables()
239267

268+
# Instant create warm-up
269+
def warm_up_instant_create(self) -> None:
270+
"""Perform required warm-up for instant table creation.
271+
272+
Must be called (and succeed) before invoking ``create_table``
273+
with ``use_instant=True``.
274+
"""
275+
self._get_odata().warm_up_instant_create()
276+
240277

241278
__all__ = ["DataverseClient"]
242279

0 commit comments

Comments
 (0)