Skip to content

Commit 2c2a7da

Browse files
author
Saurabh Badenkal
committed
Address PR review: Collection typing for filter_in/not_in, Record return types for execute()
- Changed filter_in/not_in parameter type from Sequence[Any] to Collection[Any] to accept sets, frozensets, and other non-ordered collections (Copilot review) - Updated execute() return type from Dict[str, Any] to Record to match actual runtime behavior (records.get() returns Record objects) (Copilot review) - Updated execute() docstring to reference Record objects instead of dicts - Added tests for filter_in with set and tuple inputs
1 parent 4e37cee commit 2c2a7da

3 files changed

Lines changed: 30 additions & 14 deletions

File tree

src/PowerPlatform/Dataverse/models/filters.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import enum
3737
import uuid
3838
from datetime import date, datetime, timezone
39-
from typing import Any, Sequence
39+
from typing import Any, Collection, Sequence
4040

4141
__all__ = [
4242
"FilterExpression",
@@ -220,7 +220,7 @@ class _InFilter(FilterExpression):
220220

221221
__slots__ = ("column", "values")
222222

223-
def __init__(self, column: str, values: Sequence[Any]) -> None:
223+
def __init__(self, column: str, values: Collection[Any]) -> None:
224224
if not values:
225225
raise ValueError("filter_in requires at least one value")
226226
self.column = column.lower()
@@ -241,7 +241,7 @@ class _NotInFilter(FilterExpression):
241241

242242
__slots__ = ("column", "values")
243243

244-
def __init__(self, column: str, values: Sequence[Any]) -> None:
244+
def __init__(self, column: str, values: Collection[Any]) -> None:
245245
if not values:
246246
raise ValueError("not_in requires at least one value")
247247
self.column = column.lower()
@@ -404,7 +404,7 @@ def is_not_null(column: str) -> FilterExpression:
404404
return _ComparisonFilter(column, "ne", None)
405405

406406

407-
def filter_in(column: str, values: Sequence[Any]) -> FilterExpression:
407+
def filter_in(column: str, values: Collection[Any]) -> FilterExpression:
408408
"""In filter using ``Microsoft.Dynamics.CRM.In``.
409409
410410
Named ``filter_in`` because ``in`` is a Python keyword.
@@ -422,7 +422,7 @@ def filter_in(column: str, values: Sequence[Any]) -> FilterExpression:
422422
return _InFilter(column, values)
423423

424424

425-
def not_in(column: str, values: Sequence[Any]) -> FilterExpression:
425+
def not_in(column: str, values: Collection[Any]) -> FilterExpression:
426426
"""Not-in filter using ``Microsoft.Dynamics.CRM.NotIn``.
427427
428428
Named ``not_in`` to parallel :func:`filter_in`.

src/PowerPlatform/Dataverse/models/query_builder.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@
4646

4747
from __future__ import annotations
4848

49-
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
49+
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
5050

5151
import pandas as pd
5252

5353
from . import filters
54+
from .record import Record
5455

5556
__all__ = ["QueryBuilder"]
5657

@@ -225,7 +226,7 @@ def filter_not_null(self, column: str) -> QueryBuilder:
225226

226227
# --------------------------------------------------------- filter: special
227228

228-
def filter_in(self, column: str, values: Sequence[Any]) -> QueryBuilder:
229+
def filter_in(self, column: str, values: Collection[Any]) -> QueryBuilder:
229230
"""Add an ``in`` filter using ``Microsoft.Dynamics.CRM.In``.
230231
231232
:param column: Column name (will be lowercased).
@@ -243,7 +244,7 @@ def filter_in(self, column: str, values: Sequence[Any]) -> QueryBuilder:
243244
return self
244245

245246
def filter_not_in(
246-
self, column: str, values: Sequence[Any]
247+
self, column: str, values: Collection[Any]
247248
) -> QueryBuilder:
248249
"""Add a ``not in`` filter using ``Microsoft.Dynamics.CRM.NotIn``.
249250
@@ -436,7 +437,7 @@ def build(self) -> dict:
436437

437438
# --------------------------------------------------------------- execute
438439

439-
def execute(self, *, by_page: bool = False) -> Union[Iterable[Dict[str, Any]], Iterable[List[Dict[str, Any]]]]:
440+
def execute(self, *, by_page: bool = False) -> Union[Iterable[Record], Iterable[List[Record]]]:
440441
"""Execute the query and return results.
441442
442443
By default, returns a flat iterator over individual records,
@@ -448,12 +449,14 @@ def execute(self, *, by_page: bool = False) -> Union[Iterable[Dict[str, Any]], I
448449
instances should use :meth:`build` to get parameters and pass them
449450
to ``client.records.get()`` manually.
450451
451-
:param by_page: If ``True``, yield pages (lists of record dicts)
452+
:param by_page: If ``True``, yield pages (lists of
453+
:class:`~PowerPlatform.Dataverse.models.record.Record` objects)
452454
instead of individual records. Defaults to ``False``.
453455
:type by_page: bool
454-
:return: Generator yielding individual record dicts (default) or
455-
pages of record dicts (when ``by_page=True``).
456-
:rtype: Iterable[Dict[str, Any]] or Iterable[List[Dict[str, Any]]]
456+
:return: Generator yielding individual
457+
:class:`~PowerPlatform.Dataverse.models.record.Record` objects
458+
(default) or pages of records (when ``by_page=True``).
459+
:rtype: Iterable[Record] or Iterable[List[Record]]
457460
:raises RuntimeError: If the query was not created via
458461
``client.query.builder()``.
459462
@@ -494,7 +497,7 @@ def execute(self, *, by_page: bool = False) -> Union[Iterable[Dict[str, Any]], I
494497
if by_page:
495498
return pages
496499

497-
def _flat() -> Iterable[Dict[str, Any]]:
500+
def _flat() -> Iterable[Record]:
498501
for page in pages:
499502
yield from page
500503

tests/unit/models/test_query_builder.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ def test_filter_in_returns_self(self):
125125
qb = QueryBuilder("account")
126126
self.assertIs(qb.filter_in("statecode", [0, 1]), qb)
127127

128+
def test_filter_in_accepts_set(self):
129+
qb = QueryBuilder("account").filter_in("statecode", {0, 1})
130+
result = qb.build()["filter"]
131+
self.assertIn("Microsoft.Dynamics.CRM.In", result)
132+
self.assertIn("statecode", result)
133+
134+
def test_filter_in_accepts_tuple(self):
135+
qb = QueryBuilder("account").filter_in("statecode", (0, 1, 2))
136+
self.assertEqual(
137+
qb.build()["filter"],
138+
'Microsoft.Dynamics.CRM.In(PropertyName=\'statecode\',PropertyValues=["0","1","2"])',
139+
)
140+
128141
def test_filter_in_int_enum(self):
129142
from enum import IntEnum
130143

0 commit comments

Comments
 (0)