Skip to content

Commit 87b4b44

Browse files
tpellissierclaude
andcommitted
Address Copilot review: reduce coupling, fix types, store stripped table
- Store stripped table name after validation (was validating stripped but storing original) - Change filter_in param type from list to Sequence[Any] for broader compatibility (tuples, sets) - Delegate execute() to client.records.get() instead of private _scoped_odata/_get_multiple internals - Update tests to mock records.get instead of internal OData methods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9d2ad57 commit 87b4b44

3 files changed

Lines changed: 30 additions & 48 deletions

File tree

src/PowerPlatform/Dataverse/models/query_builder.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
from __future__ import annotations
4141

42-
from typing import Any, Dict, Iterable, List, Optional, Union
42+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
4343

4444
from .filters import FilterExpression, _format_value
4545

@@ -70,7 +70,8 @@ class QueryBuilder:
7070
"""
7171

7272
def __init__(self, table: str) -> None:
73-
if not table or not table.strip():
73+
table = table.strip() if table else ""
74+
if not table:
7475
raise ValueError("table name is required")
7576
self.table = table
7677
self._select: List[str] = []
@@ -215,7 +216,7 @@ def filter_not_null(self, column: str) -> QueryBuilder:
215216

216217
# --------------------------------------------------------- filter: special
217218

218-
def filter_in(self, column: str, values: list) -> QueryBuilder:
219+
def filter_in(self, column: str, values: Sequence[Any]) -> QueryBuilder:
219220
"""Add an ``in`` filter: ``column in (val1, val2, ...)``.
220221
221222
:param column: Column name (will be lowercased).
@@ -437,23 +438,21 @@ def execute(self, *, by_page: bool = False) -> Union[Iterable[Dict[str, Any]], I
437438
params = self.build()
438439
client = self._query_ops._client
439440

440-
def _paged() -> Iterable[List[Dict[str, Any]]]:
441-
with client._scoped_odata() as od:
442-
yield from od._get_multiple(
443-
params["table"],
444-
select=params.get("select"),
445-
filter=params.get("filter"),
446-
orderby=params.get("orderby"),
447-
top=params.get("top"),
448-
expand=params.get("expand"),
449-
page_size=params.get("page_size"),
450-
)
441+
pages = client.records.get(
442+
params["table"],
443+
select=params.get("select"),
444+
filter=params.get("filter"),
445+
orderby=params.get("orderby"),
446+
top=params.get("top"),
447+
expand=params.get("expand"),
448+
page_size=params.get("page_size"),
449+
)
451450

452451
if by_page:
453-
return _paged()
452+
return pages
454453

455454
def _flat() -> Iterable[Dict[str, Any]]:
456-
for page in _paged():
455+
for page in pages:
457456
yield from page
458457

459458
return _flat()

tests/unit/models/test_query_builder.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -416,24 +416,21 @@ def test_execute_without_query_ops_raises(self):
416416
qb.execute()
417417
self.assertIn("client.query.builder()", str(ctx.exception))
418418

419-
def test_execute_calls_get_multiple(self):
420-
"""execute() should call _get_multiple via the client with built params."""
419+
def test_execute_calls_records_get(self):
420+
"""execute() should delegate to client.records.get() with built params."""
421421
mock_query_ops = MagicMock()
422422
mock_client = mock_query_ops._client
423-
mock_odata = MagicMock()
424-
mock_client._scoped_odata.return_value.__enter__ = MagicMock(return_value=mock_odata)
425-
mock_client._scoped_odata.return_value.__exit__ = MagicMock(return_value=False)
426-
mock_odata._get_multiple.return_value = iter([[{"name": "Test"}]])
423+
mock_client.records.get.return_value = iter([[{"name": "Test"}]])
427424

428425
qb = QueryBuilder("account")
429426
qb._query_ops = mock_query_ops
430427
qb.select("name", "revenue").filter_eq("statecode", 0).order_by("revenue", descending=True).top(100).page_size(
431428
50
432429
).expand("primarycontactid")
433430

434-
result = list(qb.execute())
431+
list(qb.execute())
435432

436-
mock_odata._get_multiple.assert_called_once_with(
433+
mock_client.records.get.assert_called_once_with(
437434
"account",
438435
select=["name", "revenue"],
439436
filter="statecode eq 0",
@@ -446,11 +443,7 @@ def test_execute_calls_get_multiple(self):
446443
def test_execute_returns_flat_records_by_default(self):
447444
mock_query_ops = MagicMock()
448445
mock_client = mock_query_ops._client
449-
mock_odata = MagicMock()
450-
mock_client._scoped_odata.return_value.__enter__ = MagicMock(return_value=mock_odata)
451-
mock_client._scoped_odata.return_value.__exit__ = MagicMock(return_value=False)
452-
453-
mock_odata._get_multiple.return_value = iter([[{"name": "A"}, {"name": "B"}], [{"name": "C"}]])
446+
mock_client.records.get.return_value = iter([[{"name": "A"}, {"name": "B"}], [{"name": "C"}]])
454447

455448
qb = QueryBuilder("account")
456449
qb._query_ops = mock_query_ops
@@ -464,13 +457,10 @@ def test_execute_returns_flat_records_by_default(self):
464457
def test_execute_by_page_returns_pages(self):
465458
mock_query_ops = MagicMock()
466459
mock_client = mock_query_ops._client
467-
mock_odata = MagicMock()
468-
mock_client._scoped_odata.return_value.__enter__ = MagicMock(return_value=mock_odata)
469-
mock_client._scoped_odata.return_value.__exit__ = MagicMock(return_value=False)
470460

471461
page1 = [{"name": "A"}, {"name": "B"}]
472462
page2 = [{"name": "C"}]
473-
mock_odata._get_multiple.return_value = iter([page1, page2])
463+
mock_client.records.get.return_value = iter([page1, page2])
474464

475465
qb = QueryBuilder("account")
476466
qb._query_ops = mock_query_ops
@@ -483,16 +473,13 @@ def test_execute_by_page_returns_pages(self):
483473
def test_execute_passes_none_for_empty_options(self):
484474
mock_query_ops = MagicMock()
485475
mock_client = mock_query_ops._client
486-
mock_odata = MagicMock()
487-
mock_client._scoped_odata.return_value.__enter__ = MagicMock(return_value=mock_odata)
488-
mock_client._scoped_odata.return_value.__exit__ = MagicMock(return_value=False)
489-
mock_odata._get_multiple.return_value = iter([])
476+
mock_client.records.get.return_value = iter([])
490477

491478
qb = QueryBuilder("account")
492479
qb._query_ops = mock_query_ops
493480
list(qb.execute())
494481

495-
mock_odata._get_multiple.assert_called_once_with(
482+
mock_client.records.get.assert_called_once_with(
496483
"account",
497484
select=None,
498485
filter=None,
@@ -507,17 +494,14 @@ def test_execute_with_where_expressions(self):
507494

508495
mock_query_ops = MagicMock()
509496
mock_client = mock_query_ops._client
510-
mock_odata = MagicMock()
511-
mock_client._scoped_odata.return_value.__enter__ = MagicMock(return_value=mock_odata)
512-
mock_client._scoped_odata.return_value.__exit__ = MagicMock(return_value=False)
513-
mock_odata._get_multiple.return_value = iter([])
497+
mock_client.records.get.return_value = iter([])
514498

515499
qb = QueryBuilder("account")
516500
qb._query_ops = mock_query_ops
517501
qb.where((eq("statecode", 0) | eq("statecode", 1)) & gt("revenue", 100000))
518502
list(qb.execute())
519503

520-
call_args = mock_odata._get_multiple.call_args
504+
call_args = mock_client.records.get.call_args
521505
self.assertEqual(
522506
call_args.kwargs["filter"],
523507
"((statecode eq 0 or statecode eq 1) and revenue gt 100000)",

tests/unit/test_query_operations.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,14 @@ def test_builder_execute_flat_multiple_pages(self):
9797

9898
def test_builder_execute_by_page(self):
9999
"""execute(by_page=True) should yield pages."""
100-
page1 = [{"accountid": "1"}]
101-
page2 = [{"accountid": "2"}]
102-
self.client._odata._get_multiple.return_value = iter([page1, page2])
100+
self.client._odata._get_multiple.return_value = iter([[{"accountid": "1"}], [{"accountid": "2"}]])
103101

104102
pages = list(self.client.query.builder("account").execute(by_page=True))
105103

106104
self.assertEqual(len(pages), 2)
107-
self.assertEqual(pages[0], page1)
108-
self.assertEqual(pages[1], page2)
105+
self.assertEqual(len(pages[0]), 1)
106+
self.assertEqual(pages[0][0]["accountid"], "1")
107+
self.assertEqual(pages[1][0]["accountid"], "2")
109108

110109
def test_builder_execute_all_params(self):
111110
"""builder().execute() should forward all parameters."""

0 commit comments

Comments
 (0)