Skip to content

Commit deccfa0

Browse files
Merge pull request #25 from microsoft/users/zhaodongwang/createAndGetRefactor
unify get and refactor create internal implementation
2 parents fa6ce60 + de54f6b commit deccfa0

5 files changed

Lines changed: 66 additions & 73 deletions

File tree

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
A Python package allowing developers to connect to Dataverse environments for DDL / DML operations.
44

55
- Read (SQL) — Execute constrained read-only SQL via the Dataverse Web API `?sql=` parameter. Returns `list[dict]`.
6-
- OData CRUD — Unified methods `create(logical_name, record|records)`, `update(logical_name, id|ids, patch|patches)`, `delete(logical_name, id|ids)` plus `get` / `get_multiple`.
6+
- OData CRUD — Unified methods `create(logical_name, record|records)`, `update(logical_name, id|ids, patch|patches)`, `delete(logical_name, id|ids)` plus `get` with record id or filters.
77
- Bulk create — Pass a list of records to `create(...)` to invoke the bound `CreateMultiple` action; returns `list[str]` of GUIDs. If any payload omits `@odata.type` the SDK resolves and stamps it (cached).
88
- Bulk update — Provide a list of IDs with a single patch (broadcast) or a list of per‑record patches to `update(...)`; internally uses the bound `UpdateMultiple` action; returns nothing. Each record must include the primary key attribute when sent to UpdateMultiple.
9-
- Retrieve multiple (paging) — Generator-based `get_multiple(...)` that yields pages, supports `$top` and Prefer: `odata.maxpagesize` (`page_size`).
9+
- Retrieve multiple (paging) — Generator-based `get(...)` that yields pages, supports `$top` and Prefer: `odata.maxpagesize` (`page_size`).
1010
- Upload files — Call `upload_file(logical_name, ...)` and an upload method will be auto picked (you can override the mode). See https://learn.microsoft.com/en-us/power-apps/developer/data-platform/file-column-data?tabs=sdk#upload-files
1111
- Metadata helpers — Create/inspect/delete simple custom tables (EntityDefinitions + Attributes).
1212
- Pandas helpers — Convenience DataFrame oriented wrappers for quick prototyping/notebooks.
@@ -19,7 +19,7 @@ A Python package allowing developers to connect to Dataverse environments for DD
1919
- Table metadata ops: create simple custom tables (supports string/int/decimal/float/datetime/bool/optionset) and delete them.
2020
- Bulk create via `CreateMultiple` (collection-bound) by passing `list[dict]` to `create(logical_name, payloads)`; returns list of created IDs.
2121
- Bulk update via `UpdateMultiple` (invoked internally) by calling unified `update(logical_name, ids, patch|patches)`; returns nothing.
22-
- 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`).
22+
- Retrieve multiple with server-driven paging: `get(...)` yields lists (pages) following `@odata.nextLink`. Control total via `$top` and per-page via `page_size` (Prefer: `odata.maxpagesize`).
2323
- Upload files, using either a single request (supports file size up to 128 MB) or chunk upload under the hood
2424
- Optional pandas integration (`PandasODataClient`) for DataFrame based create / get / query.
2525

@@ -35,7 +35,7 @@ Auth:
3535
| `create` | `create(logical_name, record_dict)` | `list[str]` (len 1) | Single create; GUID from `OData-EntityId`. |
3636
| `create` | `create(logical_name, list[record_dict])` | `list[str]` | Uses `CreateMultiple`; stamps `@odata.type` if missing. |
3737
| `get` | `get(logical_name, id)` | `dict` | One record; supply GUID (with/without parentheses). |
38-
| `get_multiple` | `get_multiple(logical_name, ..., page_size=None)` | `Iterable[list[dict]]` | Pages yielded (non-empty only). |
38+
| `get` | `get(logical_name, ..., page_size=None)` | `Iterable[list[dict]]` | Multiple records; Pages yielded (non-empty only). |
3939
| `update` | `update(logical_name, id, patch)` | `None` | Single update; no representation returned. |
4040
| `update` | `update(logical_name, list[id], patch)` | `None` | Broadcast; same patch applied to all IDs (UpdateMultiple). |
4141
| `update` | `update(logical_name, list[id], list[patch])` | `None` | 1:1 patches; lengths must match (UpdateMultiple). |
@@ -216,10 +216,10 @@ Notes:
216216

217217
## Retrieve multiple with paging
218218

219-
Use `get_multiple(logical_name, ...)` 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`).
219+
Use `get(logical_name, ...)` 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`).
220220

221221
```python
222-
pages = client.get_multiple(
222+
pages = client.get(
223223
"account",
224224
select=["accountid", "name", "createdon"],
225225
orderby=["name asc"],
@@ -255,7 +255,7 @@ Return value & semantics
255255
Example (all parameters + expected response)
256256

257257
```python
258-
pages = client.get_multiple(
258+
pages = client.get(
259259
"account",
260260
select=["accountid", "name", "createdon", "primarycontactid"],
261261
filter="contains(name,'Acme') and statecode eq 0",
@@ -338,7 +338,7 @@ Notes:
338338
- `create` always returns a list of GUIDs (length 1 for single input).
339339
- `update` and `delete` return `None` for both single and multi.
340340
- Passing a list of payloads to `create` triggers bulk create and returns `list[str]` of IDs.
341-
- Use `get_multiple` for paging through result sets; prefer `select` to limit columns.
341+
- `get` supports single record retrieval with record id or paging through result sets (prefer `select` to limit columns).
342342
- For CRUD methods that take a record id, pass the GUID string (36-char hyphenated). Parentheses around the GUID are accepted but not required.
343343
* SQL queries are executed directly against entity set endpoints using the `?sql=` parameter. Supported subset only (single SELECT, optional WHERE/TOP/ORDER BY, alias). Unsupported constructs will be rejected by the service.
344344

examples/quickstart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def run_paging_demo(label: str, *, top: Optional[int], page_size: Optional[int])
441441
page_index = 0
442442
_select = [id_key, code_key, amount_key, when_key, status_key]
443443
_orderby = [f"{code_key} asc"]
444-
for page in client.get_multiple(
444+
for page in client.get(
445445
logical,
446446
select=_select,
447447
filter=None,

src/dataverse_sdk/client.py

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,15 @@ def create(self, logical_name: str, records: Union[Dict[str, Any], List[Dict[str
8383
List of created GUIDs (length 1 for single input).
8484
"""
8585
od = self._get_odata()
86+
entity_set = od._entity_set_from_logical(logical_name)
8687
if isinstance(records, dict):
87-
rid = od._create(logical_name, records)
88+
rid = od._create(entity_set, logical_name, records)
8889
# _create returns str on single input
8990
if not isinstance(rid, str):
9091
raise TypeError("_create (single) did not return GUID string")
9192
return [rid]
9293
if isinstance(records, list):
93-
ids = od._create(logical_name, records)
94+
ids = od._create_multiple(entity_set, logical_name, records)
9495
if not isinstance(ids, list) or not all(isinstance(x, str) for x in ids):
9596
raise TypeError("_create (multi) did not return list[str]")
9697
return ids
@@ -131,39 +132,28 @@ def delete(self, logical_name: str, ids: Union[str, List[str]]) -> None:
131132
od._delete_multiple(logical_name, ids)
132133
return None
133134

134-
def get(self, logical_name: str, record_id: str) -> dict:
135-
"""Fetch a record by ID.
136-
137-
Parameters
138-
----------
139-
logical_name : str
140-
Logical (singular) entity name.
141-
record_id : str
142-
The record GUID (with or without parentheses).
143-
144-
Returns
145-
-------
146-
dict
147-
The record JSON payload.
148-
"""
149-
return self._get_odata()._get(logical_name, record_id)
150-
151-
def get_multiple(
135+
def get(
152136
self,
153137
logical_name: str,
138+
record_id: Optional[str] = None,
154139
select: Optional[List[str]] = None,
155140
filter: Optional[str] = None,
156141
orderby: Optional[List[str]] = None,
157142
top: Optional[int] = None,
158143
expand: Optional[List[str]] = None,
159144
page_size: Optional[int] = None,
160-
) -> Iterable[List[Dict[str, Any]]]:
161-
"""Fetch multiple records page-by-page as a generator.
162-
163-
Yields a list of records per page, following @odata.nextLink until exhausted.
164-
Parameters mirror standard OData query options.
165-
"""
166-
return self._get_odata()._get_multiple(
145+
) -> Union[Dict[str, Any], Iterable[List[Dict[str, Any]]]]:
146+
"""Fetch single record by ID or multiple records as a generator."""
147+
od = self._get_odata()
148+
if record_id is not None:
149+
if not isinstance(record_id, str):
150+
raise TypeError("record_id must be str")
151+
return od._get(
152+
logical_name,
153+
record_id,
154+
select=select,
155+
)
156+
return od._get_multiple(
167157
logical_name,
168158
select=select,
169159
filter=filter,

src/dataverse_sdk/odata.py

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -118,44 +118,26 @@ def _request(self, method: str, url: str, *, expected: tuple[int, ...] = (200, 2
118118
is_transient=is_transient,
119119
)
120120

121-
# ----------------------------- CRUD ---------------------------------
122-
def _create(self, logical_name: str, data: Union[Dict[str, Any], List[Dict[str, Any]]]) -> Union[str, List[str]]:
123-
"""Create one or many records by logical (singular) name.
121+
# --- CRUD Internal functions ---
122+
def _create(self, entity_set: str, logical_name: str, record: Dict[str, Any]) -> str:
123+
"""Create a single record and return its GUID.
124124
125125
Parameters
126-
----------
126+
-------
127+
entity_set : str
128+
Resolved entity set (plural) name.
127129
logical_name : str
128-
Logical (singular) entity name, e.g. "account".
129-
data : dict | list[dict]
130-
Single entity payload or list of payloads for batch create.
131-
132-
Behaviour
133-
---------
134-
- Resolves entity set once per call via metadata (cached) then issues requests.
135-
- Single (dict): POST /{entity_set}. Returns GUID string (no representation fetched).
136-
- Multiple (list[dict]): POST /{entity_set}/Microsoft.Dynamics.CRM.CreateMultiple. Returns list[str] of created GUIDs.
137-
138-
Multi-create logical name resolution
139-
------------------------------------
140-
- If any payload omits ``@odata.type`` the client stamps ``Microsoft.Dynamics.CRM.<logical_name>``.
141-
- If all payloads already include ``@odata.type`` no modification occurs.
130+
Singular logical entity name.
131+
record : dict[str, Any]
132+
Attribute payload mapped by logical column names.
142133
143134
Returns
144135
-------
145-
str | list[str]
146-
Created record GUID (single) or list of created IDs (multi).
147-
"""
148-
entity_set = self._entity_set_from_logical(logical_name)
149-
if isinstance(data, dict):
150-
return self._create_single(entity_set, logical_name, data)
151-
if isinstance(data, list):
152-
return self._create_multiple(entity_set, logical_name, data)
153-
raise TypeError("data must be dict or list[dict]")
154-
155-
# --- Internal helpers ---
156-
def _create_single(self, entity_set: str, logical_name: str, record: Dict[str, Any]) -> str:
157-
"""Create a single record and return its GUID.
136+
str
137+
Created record GUID.
158138
139+
Notes
140+
-------
159141
Relies on OData-EntityId (canonical) or Location header. No response body parsing is performed.
160142
Raises RuntimeError if neither header contains a GUID.
161143
"""
@@ -179,6 +161,27 @@ def _create_single(self, entity_set: str, logical_name: str, record: Dict[str, A
179161
)
180162

181163
def _create_multiple(self, entity_set: str, logical_name: str, records: List[Dict[str, Any]]) -> List[str]:
164+
"""Create multiple records using the collection-bound CreateMultiple action.
165+
166+
Parameters
167+
----------
168+
entity_set : str
169+
Resolved entity set (plural) name.
170+
logical_name : str
171+
Singular logical entity name.
172+
records : list[dict[str, Any]]
173+
Payloads mapped by logical attribute names.
174+
175+
Multi-create logical name resolution
176+
------------------------------------
177+
- If any payload omits ``@odata.type`` the client stamps ``Microsoft.Dynamics.CRM.<logical_name>``.
178+
- If all payloads already include ``@odata.type`` no modification occurs.
179+
180+
Returns
181+
-------
182+
list[str]
183+
List of created IDs.
184+
"""
182185
if not all(isinstance(r, dict) for r in records):
183186
raise TypeError("All items for multi-create must be dicts")
184187
need_logical = any("@odata.type" not in r for r in records)

tests/test_create_single_guid.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,23 @@ def __init__(self, headers):
3232
def _convert_labels_to_ints(self, logical_name, record): # pragma: no cover - test shim
3333
return record
3434

35-
def test__create_single_uses_odata_entityid():
35+
def test__create_uses_odata_entityid():
3636
guid = "11111111-2222-3333-4444-555555555555"
3737
headers = {"OData-EntityId": f"https://org.example/api/data/v9.2/accounts({guid})"}
3838
c = TestableOData(headers)
3939
# Current signature requires logical name explicitly
40-
result = c._create_single("accounts", "account", {"name": "x"})
40+
result = c._create("accounts", "account", {"name": "x"})
4141
assert result == guid
4242

43-
def test__create_single_fallback_location():
43+
def test__create_fallback_location():
4444
guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
4545
headers = {"Location": f"https://org.example/api/data/v9.2/contacts({guid})"}
4646
c = TestableOData(headers)
47-
result = c._create_single("contacts", "contact", {"firstname": "x"})
47+
result = c._create("contacts", "contact", {"firstname": "x"})
4848
assert result == guid
4949

50-
def test__create_single_missing_headers_raises():
50+
def test__create_missing_headers_raises():
5151
c = TestableOData({})
5252
import pytest
5353
with pytest.raises(RuntimeError):
54-
c._create_single("accounts", "account", {"name": "x"})
54+
c._create("accounts", "account", {"name": "x"})

0 commit comments

Comments
 (0)