Skip to content

Commit 4dad4bd

Browse files
tpellissier-msfttpellissierclaude
authored
Move paginated query from client.query.get() to client.records.get() overload (#107)
- Add @overload to records.get(): with record_id returns dict, without returns paginated generator - Remove get() from QueryOperations (client.query now only has sql()) - Update deprecated client.get() to route both cases to records.get() - Update all examples, README, and tests Co-authored-by: tpellissier <tpellissier@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dafa6a0 commit 4dad4bd

File tree

10 files changed

+245
-157
lines changed

10 files changed

+245
-157
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ The SDK provides a simple, pythonic interface for Dataverse operations:
112112
| Concept | Description |
113113
|---------|-------------|
114114
| **DataverseClient** | Main entry point; provides `records`, `query`, and `tables` namespaces |
115-
| **Namespaces** | Operations are organized into `client.records` (CRUD), `client.query` (queries), and `client.tables` (metadata & relationships) |
115+
| **Namespaces** | Operations are organized into `client.records` (CRUD & OData queries), `client.query` (query & search), and `client.tables` (metadata) |
116116
| **Records** | Dataverse records represented as Python dictionaries with column schema names |
117117
| **Schema names** | Use table schema names (`"account"`, `"new_MyTestTable"`) and column schema names (`"name"`, `"new_MyTestColumn"`). See: [Table definitions in Microsoft Dataverse](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/entity-metadata) |
118118
| **Bulk Operations** | Efficient bulk processing for multiple records with automatic optimization |
@@ -190,7 +190,7 @@ for record in results:
190190

191191
# OData query with paging
192192
# Note: filter and expand parameters are case sensitive
193-
for page in client.query.get(
193+
for page in client.records.get(
194194
"account",
195195
select=["accountid", "name"], # select is case-insensitive (automatically lowercased)
196196
filter="statecode eq 0", # filter must use lowercase logical names (not transformed)
@@ -200,7 +200,7 @@ for page in client.query.get(
200200
print(record["name"])
201201

202202
# Query with navigation property expansion (case-sensitive!)
203-
for page in client.query.get(
203+
for page in client.records.get(
204204
"account",
205205
select=["name"],
206206
expand=["primarycontactid"], # Navigation property names are case-sensitive

examples/advanced/walkthrough.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ def main():
193193
)
194194

195195
# Multiple read with filter
196-
log_call(f"client.query.get('{table_name}', filter='new_quantity gt 5')")
196+
log_call(f"client.records.get('{table_name}', filter='new_quantity gt 5')")
197197
all_records = []
198-
records_iterator = backoff(lambda: client.query.get(table_name, filter="new_quantity gt 5"))
198+
records_iterator = backoff(lambda: client.records.get(table_name, filter="new_quantity gt 5"))
199199
for page in records_iterator:
200200
all_records.extend(page)
201201
print(f"[OK] Found {len(all_records)} records with new_quantity > 5")
@@ -243,9 +243,9 @@ def main():
243243
print(f"[OK] Created {len(paging_ids)} records for paging demo")
244244

245245
# Query with paging
246-
log_call(f"client.query.get('{table_name}', page_size=5)")
246+
log_call(f"client.records.get('{table_name}', page_size=5)")
247247
print("Fetching records with page_size=5...")
248-
paging_iterator = backoff(lambda: client.query.get(table_name, orderby=["new_Quantity"], page_size=5))
248+
paging_iterator = backoff(lambda: client.records.get(table_name, orderby=["new_Quantity"], page_size=5))
249249
for page_num, page in enumerate(paging_iterator, start=1):
250250
record_ids = [r.get("new_walkthroughdemoid")[:8] + "..." for r in page]
251251
print(f" Page {page_num}: {len(page)} records - IDs: {record_ids}")

examples/basic/functional_testing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def test_query_records(client: DataverseClient, table_info: Dict[str, Any]) -> N
271271
print("Querying records from test table...")
272272
for attempt in range(1, retries + 1):
273273
try:
274-
records_iterator = client.query.get(
274+
records_iterator = client.records.get(
275275
table_schema_name,
276276
select=[f"{attr_prefix}_name", f"{attr_prefix}_count", f"{attr_prefix}_amount"],
277277
filter=f"{attr_prefix}_is_active eq true",

examples/basic/installation_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def show_usage_examples():
218218
Querying Data:
219219
```python
220220
# Query with OData filter
221-
accounts = client.query.get("account",
221+
accounts = client.records.get("account",
222222
filter="name eq 'Contoso Ltd'",
223223
select=["name", "telephone1"],
224224
top=10)

src/PowerPlatform/Dataverse/client.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class DataverseClient:
5353
5454
Operations are organized into namespaces:
5555
56-
- ``client.records`` -- create, update, delete, get individual records
57-
- ``client.query`` -- paginated OData queries and read-only SQL queries (via Web API ``?sql=`` parameter)
56+
- ``client.records`` -- create, update, delete, and get records (single or paginated queries)
57+
- ``client.query`` -- query and search operations
5858
- ``client.tables`` -- table and column metadata management
5959
6060
Example:
@@ -76,7 +76,7 @@ class DataverseClient:
7676
client.records.update("account", record_id, {"telephone1": "555-0100"})
7777
7878
# Query records
79-
for page in client.query.get("account", filter="name eq 'Contoso Ltd'"):
79+
for page in client.records.get("account", filter="name eq 'Contoso Ltd'"):
8080
for account in page:
8181
print(account["name"])
8282
@@ -283,10 +283,10 @@ def get(
283283
) -> Union[Dict[str, Any], Iterable[List[Dict[str, Any]]]]:
284284
"""
285285
.. note::
286-
Deprecated. This method has been split into two:
286+
Deprecated. Use :meth:`~PowerPlatform.Dataverse.operations.records.RecordOperations.get` instead.
287287
288-
- **Single record by ID** -- use ``client.records.get(table, record_id)``
289-
- **Query / filter multiple records** -- use ``client.query.get(table, filter=..., select=...)``
288+
- **Single record by ID** -- ``client.records.get(table, record_id)``
289+
- **Query / filter multiple records** -- ``client.records.get(table, filter=..., select=...)``
290290
291291
Fetch a single record by ID or query multiple records.
292292
@@ -354,14 +354,14 @@ def get(
354354
print(f"Batch size: {len(batch)}")
355355
"""
356356
warnings.warn(
357-
"client.get() is deprecated. Use client.records.get() or client.query.get() instead.",
357+
"client.get() is deprecated. Use client.records.get() instead.",
358358
DeprecationWarning,
359359
stacklevel=2,
360360
)
361361
if record_id is not None:
362362
return self.records.get(table_schema_name, record_id, select=select)
363363
else:
364-
return self.query.get(
364+
return self.records.get(
365365
table_schema_name,
366366
select=select,
367367
filter=filter,

src/PowerPlatform/Dataverse/operations/query.py

Lines changed: 4 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
from typing import Any, Dict, Iterable, List, Optional, TYPE_CHECKING
8+
from typing import Any, Dict, List, TYPE_CHECKING
99

1010
if TYPE_CHECKING:
1111
from ..client import DataverseClient
@@ -15,10 +15,10 @@
1515

1616

1717
class QueryOperations:
18-
"""Namespace for query operations (multi-record retrieval and SQL).
18+
"""Namespace for query operations.
1919
20-
Accessed via ``client.query``. Provides paginated OData queries and SQL
21-
query execution against Dataverse tables.
20+
Accessed via ``client.query``. Provides query and search operations
21+
against Dataverse tables.
2222
2323
:param client: The parent :class:`~PowerPlatform.Dataverse.client.DataverseClient` instance.
2424
:type client: ~PowerPlatform.Dataverse.client.DataverseClient
@@ -27,12 +27,6 @@ class QueryOperations:
2727
2828
client = DataverseClient(base_url, credential)
2929
30-
# Paginated query
31-
for page in client.query.get("account", select=["name"], top=100):
32-
for record in page:
33-
print(record["name"])
34-
35-
# SQL query
3630
rows = client.query.sql("SELECT TOP 10 name FROM account ORDER BY name")
3731
for row in rows:
3832
print(row["name"])
@@ -41,89 +35,6 @@ class QueryOperations:
4135
def __init__(self, client: DataverseClient) -> None:
4236
self._client = client
4337

44-
# -------------------------------------------------------------------- get
45-
46-
def get(
47-
self,
48-
table: str,
49-
*,
50-
select: Optional[List[str]] = None,
51-
filter: Optional[str] = None,
52-
orderby: Optional[List[str]] = None,
53-
top: Optional[int] = None,
54-
expand: Optional[List[str]] = None,
55-
page_size: Optional[int] = None,
56-
) -> Iterable[List[Dict[str, Any]]]:
57-
"""Query multiple records from a Dataverse table with pagination.
58-
59-
Returns a generator that yields one page (list of record dicts) at a
60-
time. Automatically follows ``@odata.nextLink`` for server-side paging.
61-
62-
:param table: Schema name of the table (e.g. ``"account"`` or
63-
``"new_MyTestTable"``).
64-
:type table: :class:`str`
65-
:param select: Optional list of column logical names to include.
66-
Column names are automatically lowercased.
67-
:type select: :class:`list` of :class:`str` or None
68-
:param filter: Optional OData ``$filter`` expression (e.g.
69-
``"name eq 'Contoso'"``). Column names in filter expressions must
70-
use exact lowercase logical names. Passed directly without
71-
transformation.
72-
:type filter: :class:`str` or None
73-
:param orderby: Optional list of sort expressions (e.g.
74-
``["name asc", "createdon desc"]``). Column names are automatically
75-
lowercased.
76-
:type orderby: :class:`list` of :class:`str` or None
77-
:param top: Optional maximum total number of records to return.
78-
:type top: :class:`int` or None
79-
:param expand: Optional list of navigation properties to expand (e.g.
80-
``["primarycontactid"]``). Case-sensitive; must match server-defined
81-
names exactly.
82-
:type expand: :class:`list` of :class:`str` or None
83-
:param page_size: Optional per-page size hint sent via
84-
``Prefer: odata.maxpagesize``.
85-
:type page_size: :class:`int` or None
86-
87-
:return: Generator yielding pages, where each page is a list of record
88-
dictionaries.
89-
:rtype: :class:`collections.abc.Iterable` of :class:`list` of :class:`dict`
90-
91-
Example:
92-
Query with filtering and pagination::
93-
94-
for page in client.query.get(
95-
"account",
96-
filter="statecode eq 0",
97-
select=["name", "telephone1"],
98-
page_size=50,
99-
):
100-
for record in page:
101-
print(record["name"])
102-
103-
Query with sorting and limit::
104-
105-
for page in client.query.get(
106-
"account",
107-
orderby=["createdon desc"],
108-
top=100,
109-
):
110-
print(f"Page size: {len(page)}")
111-
"""
112-
113-
def _paged() -> Iterable[List[Dict[str, Any]]]:
114-
with self._client._scoped_odata() as od:
115-
yield from od._get_multiple(
116-
table,
117-
select=select,
118-
filter=filter,
119-
orderby=orderby,
120-
top=top,
121-
expand=expand,
122-
page_size=page_size,
123-
)
124-
125-
return _paged()
126-
12738
# -------------------------------------------------------------------- sql
12839

12940
def sql(self, sql: str) -> List[Dict[str, Any]]:

0 commit comments

Comments
 (0)