Skip to content

Commit c7b16a6

Browse files
Merge pull request #6 from microsoft/user/tpellissier/bulkops
Bulk create (CreateMultiple), paged retrieves, README/quickstart updates
2 parents d8df99b + 34df514 commit c7b16a6

4 files changed

Lines changed: 472 additions & 95 deletions

File tree

README.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ A minimal Python SDK to use Microsoft Dataverse as a database for Azure AI Found
44

55
- Read (SQL) — Execute read-only T‑SQL via the McpExecuteSqlQuery Custom API. Returns `list[dict]`.
66
- OData CRUD — Thin wrappers over Dataverse Web API (create/get/update/delete).
7+
- Bulk create — Pass a list of records to `create(...)` to invoke the bound `CreateMultiple` action; returns `list[str]` of GUIDs.
8+
- Retrieve multiple (paging) — Generator-based `get_multiple(...)` that yields pages, supports `$top` and Prefer: `odata.maxpagesize` (`page_size`).
79
- Metadata helpers — Create/inspect/delete simple custom tables (EntityDefinitions + Attributes).
810
- Pandas helpers — Convenience DataFrame oriented wrappers for quick prototyping/notebooks.
911
- Auth — Azure Identity (`TokenCredential`) injection.
@@ -13,6 +15,8 @@ A minimal Python SDK to use Microsoft Dataverse as a database for Azure AI Found
1315
- Simple `DataverseClient` facade for CRUD, SQL (read-only), and table metadata.
1416
- SQL-over-API: T-SQL routed through Custom API endpoint (no ODBC / TDS driver required).
1517
- Table metadata ops: create simple custom tables with primitive columns (string/int/decimal/float/datetime/bool) and delete them.
18+
- Bulk create via `CreateMultiple` (collection-bound) by passing `list[dict]` to `create(entity_set, payloads)`; returns list of created IDs.
19+
- Retrieve multiple with server-driven paging: `get_multiple(...)` yields lists (pages) following `@odata.nextLink`. Control total via `$top` and per-page via `page_size` (Prefer: `odata.maxpagesize`).
1620
- Optional pandas integration (`PandasODataClient`) for DataFrame based create / get / query.
1721

1822
Auth:
@@ -62,6 +66,8 @@ python examples/quickstart.py
6266
The quickstart demonstrates:
6367
- Creating a simple custom table (metadata APIs)
6468
- Creating, reading, updating, and deleting records (OData)
69+
- Bulk create (CreateMultiple) to insert many records in one call
70+
- Retrieve multiple with paging (contrasting `$top` vs `page_size`)
6571
- Executing a read-only SQL query
6672

6773
## Examples
@@ -101,6 +107,95 @@ client.delete("accounts", account_id)
101107
rows = client.query_sql("SELECT TOP 3 accountid, name FROM account ORDER BY createdon DESC")
102108
for r in rows:
103109
print(r.get("accountid"), r.get("name"))
110+
111+
## Bulk create (CreateMultiple)
112+
113+
Pass a list of payloads to `create(entity_set, payloads)` to invoke the collection-bound `Microsoft.Dynamics.CRM.CreateMultiple` action. The method returns a `list[str]` of created record IDs.
114+
115+
```python
116+
# Bulk create accounts (returns list of GUIDs)
117+
payloads = [
118+
{"name": "Contoso"},
119+
{"name": "Fabrikam"},
120+
{"name": "Northwind"},
121+
]
122+
ids = client.create("accounts", payloads)
123+
assert isinstance(ids, list) and all(isinstance(x, str) for x in ids)
124+
print({"created_ids": ids})
125+
```
126+
127+
Notes:
128+
- The bulk create response typically includes IDs only; the SDK returns the list of GUID strings.
129+
- Single-record `create` still returns the full entity representation.
130+
131+
## Retrieve multiple with paging
132+
133+
Use `get_multiple(entity_set, ...)` to stream results page-by-page. You can cap total results with `$top` and hint the per-page size with `page_size` (sets Prefer: `odata.maxpagesize`).
134+
135+
```python
136+
# Iterate pages of accounts ordered by name, selecting a few columns
137+
pages = client.get_multiple(
138+
"accounts",
139+
select=["accountid", "name", "createdon"],
140+
orderby=["name asc"],
141+
top=10, # stop after 10 total rows (optional)
142+
page_size=3, # ask for ~3 per page (optional)
143+
)
144+
145+
total = 0
146+
for page in pages: # each page is a list[dict]
147+
print({"page_size": len(page), "sample": page[:2]})
148+
total += len(page)
149+
print({"total_rows": total})
150+
```
151+
152+
Parameters
153+
- `entity`: str — Entity set name (plural logical name), e.g., `"accounts"`.
154+
- `select`: list[str] | None — Columns to include; joined into `$select`.
155+
- `filter`: str | None — OData `$filter` expression (e.g., `"contains(name,'Acme') and statecode eq 0"`).
156+
- `orderby`: list[str] | None — Sort expressions (e.g., `["name asc", "createdon desc"]`).
157+
- `top`: int | None — Cap on total rows across all pages (sent as `$top` on first request).
158+
- `expand`: list[str] | None — Navigation expansions as raw OData strings. Example:
159+
- `"primarycontactid($select=fullname,emailaddress1)"`
160+
- `"parentaccountid($select=name)"`
161+
- `page_size`: int | None — Per-page hint via Prefer: `odata.maxpagesize=N`.
162+
163+
Example (all parameters + expected response)
164+
165+
```python
166+
pages = client.get_multiple(
167+
"accounts",
168+
select=["accountid", "name", "createdon", "primarycontactid"],
169+
filter="contains(name,'Acme') and statecode eq 0",
170+
orderby=["name asc", "createdon desc"],
171+
top=5,
172+
expand=["primarycontactid($select=fullname,emailaddress1)"],
173+
page_size=2,
174+
)
175+
176+
for page in pages: # page is list[dict]
177+
# Expected page shape (illustrative):
178+
# [
179+
# {
180+
# "accountid": "00000000-0000-0000-0000-000000000001",
181+
# "name": "Acme West",
182+
# "createdon": "2025-08-01T12:34:56Z",
183+
# "primarycontactid": {
184+
# "contactid": "00000000-0000-0000-0000-0000000000aa",
185+
# "fullname": "Jane Doe",
186+
# "emailaddress1": "jane@acme.com"
187+
# },
188+
# "@odata.etag": "W/\"123456\""
189+
# },
190+
# ...
191+
# ]
192+
print({"page_size": len(page)})
193+
```
194+
195+
Semantics:
196+
- `$top`: caps the total number of rows returned across all pages.
197+
- `page_size`: per-page size hint via Prefer: `odata.maxpagesize`; the service may return fewer/more.
198+
- The generator follows `@odata.nextLink` until exhausted or `$top` is satisfied.
104199
```
105200
106201
### Custom table (metadata) example
@@ -136,6 +231,8 @@ client.delete_table("SampleItem") # delete the table
136231

137232
Notes:
138233
- `create/update` return the full record using `Prefer: return=representation`.
234+
- Passing a list of payloads to `create` triggers bulk create and returns `list[str]` of IDs.
235+
- Use `get_multiple` for paging through result sets; prefer `select` to limit columns.
139236
- For CRUD methods that take a record id, pass the GUID string (36-char hyphenated). Parentheses around the GUID are accepted but not required.
140237
- SQL is routed through the Custom API named in `DataverseConfig.sql_api_name` (default: `McpExecuteSqlQuery`).
141238

@@ -148,7 +245,8 @@ VS Code Tasks
148245
- Run example: `Run Quickstart (Dataverse SDK)`
149246

150247
## Limitations / Future Work
151-
- No batching, upsert, or association operations yet.
248+
- No general-purpose OData batching, upsert, or association operations yet.
249+
- `DeleteMultiple`/`UpdateMultiple` are not exposed; quickstart may demonstrate faster deletes using client-side concurrency only.
152250
- Minimal retry policy in library (network-error only); examples include additional backoff for transient Dataverse consistency.
153251
- Entity naming conventions in Dataverse (schema/logical/entity set plural & publisher prefix) using the SDK is currently not well-defined
154252

0 commit comments

Comments
 (0)