Skip to content

Commit 8c38842

Browse files
author
Abel Milash
committed
Address PR review: refactor QueryBuilder to delegate to filters module, remove filter_in (OData 4.01 not supported by Dataverse)
1 parent 5425cf4 commit 8c38842

File tree

6 files changed

+51
-178
lines changed

6 files changed

+51
-178
lines changed

README.md

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,34 @@ A Python client library for Microsoft Dataverse that provides a unified interfac
1313
1414
## Table of contents
1515

16-
- [Key features](#key-features)
17-
- [Getting started](#getting-started)
18-
- [Prerequisites](#prerequisites)
19-
- [Install the package](#install-the-package)
20-
- [Authenticate the client](#authenticate-the-client)
21-
- [Key concepts](#key-concepts)
22-
- [Examples](#examples)
23-
- [Quick start](#quick-start)
24-
- [Basic CRUD operations](#basic-crud-operations)
25-
- [Bulk operations](#bulk-operations)
26-
- [Upsert operations](#upsert-operations)
27-
- [Query data](#query-data) *(QueryBuilder, SQL, raw OData)*
28-
- [Table management](#table-management)
29-
- [Relationship management](#relationship-management)
30-
- [File operations](#file-operations)
31-
- [Next steps](#next-steps)
32-
- [Troubleshooting](#troubleshooting)
33-
- [Contributing](#contributing)
16+
- [PowerPlatform Dataverse Client for Python](#powerplatform-dataverse-client-for-python)
17+
- [Table of contents](#table-of-contents)
18+
- [Key features](#key-features)
19+
- [Getting started](#getting-started)
20+
- [Prerequisites](#prerequisites)
21+
- [Install the package](#install-the-package)
22+
- [Authenticate the client](#authenticate-the-client)
23+
- [Key concepts](#key-concepts)
24+
- [Examples](#examples)
25+
- [Quick start](#quick-start)
26+
- [Basic CRUD operations](#basic-crud-operations)
27+
- [Bulk operations](#bulk-operations)
28+
- [Upsert operations](#upsert-operations)
29+
- [Query data](#query-data)
30+
- [Table management](#table-management)
31+
- [Relationship management](#relationship-management)
32+
- [File operations](#file-operations)
33+
- [Next steps](#next-steps)
34+
- [More sample code](#more-sample-code)
35+
- [Additional documentation](#additional-documentation)
36+
- [Troubleshooting](#troubleshooting)
37+
- [General](#general)
38+
- [Authentication issues](#authentication-issues)
39+
- [Performance considerations](#performance-considerations)
40+
- [Limitations](#limitations)
41+
- [Contributing](#contributing)
42+
- [API Design Guidelines](#api-design-guidelines)
43+
- [Trademarks](#trademarks)
3444

3545
## Key features
3646

@@ -258,7 +268,6 @@ query = (client.query.builder("contact")
258268
.filter_eq("statecode", 0) # statecode eq 0
259269
.filter_gt("revenue", 1000000) # revenue gt 1000000
260270
.filter_contains("name", "Corp") # contains(name, 'Corp')
261-
.filter_in("statecode", [0, 1]) # statecode in (0, 1)
262271
.filter_between("revenue", 100000, 500000) # (revenue ge 100000 and revenue le 500000)
263272
.filter_null("telephone1") # telephone1 eq null
264273
)
@@ -267,7 +276,7 @@ query = (client.query.builder("contact")
267276
For complex logic (OR, NOT, grouping), use the composable expression tree with `where()`:
268277

269278
```python
270-
from PowerPlatform.Dataverse.models.filters import eq, gt, filter_in, between
279+
from PowerPlatform.Dataverse.models.filters import eq, gt, between
271280

272281
# OR conditions: (statecode = 0 OR statecode = 1) AND revenue > 100k
273282
for record in (client.query.builder("account")

examples/advanced/walkthrough.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -278,21 +278,6 @@ def _run_walkthrough(client):
278278
for rec in qb_records[:5]:
279279
print(f" - '{rec.get('new_title')}' Amount={rec.get('new_amount')}")
280280

281-
# filter_in: records with specific priorities
282-
log_call("client.query.builder(...).filter_in('new_Priority', [HIGH, LOW]).execute()")
283-
print("Querying records with HIGH or LOW priority (filter_in)...")
284-
priority_records = list(
285-
backoff(
286-
lambda: client.query.builder(table_name)
287-
.select("new_Title", "new_Priority")
288-
.filter_in("new_Priority", [Priority.HIGH, Priority.LOW])
289-
.execute()
290-
)
291-
)
292-
print(f"[OK] Found {len(priority_records)} records with HIGH or LOW priority")
293-
for rec in priority_records[:5]:
294-
print(f" - '{rec.get('new_title')}' Priority={rec.get('new_priority')}")
295-
296281
# filter_between: amount in a range
297282
log_call("client.query.builder(...).filter_between('new_Amount', 500, 1500).execute()")
298283
print("Querying records with amount between 500 and 1500 (filter_between)...")
@@ -447,7 +432,7 @@ def _run_walkthrough(client):
447432
print(" [OK] Reading records by ID and with filters")
448433
print(" [OK] Single and multiple record updates")
449434
print(" [OK] Paging through large result sets")
450-
print(" [OK] QueryBuilder fluent queries (filter_eq, filter_in, filter_between, where)")
435+
print(" [OK] QueryBuilder fluent queries (filter_eq, filter_between, where)")
451436
print(" [OK] SQL queries")
452437
print(" [OK] Picklist label-to-value conversion")
453438
print(" [OK] Column management")

src/PowerPlatform/Dataverse/models/filters.py

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
Example::
1212
13-
from PowerPlatform.Dataverse.models.filters import eq, gt, filter_in
13+
from PowerPlatform.Dataverse.models.filters import eq, gt, between
1414
1515
# Simple comparison
1616
expr = eq("statecode", 0)
@@ -21,10 +21,6 @@
2121
print(expr.to_odata())
2222
# ((statecode eq 0 or statecode eq 1) and revenue gt 100000)
2323
24-
# In operator
25-
expr = filter_in("statecode", [0, 1, 2])
26-
print(expr.to_odata()) # statecode in (0, 1, 2)
27-
2824
# Negation
2925
expr = ~eq("statecode", 1)
3026
print(expr.to_odata()) # not (statecode eq 1)
@@ -35,7 +31,7 @@
3531
import enum
3632
import uuid
3733
from datetime import date, datetime, timezone
38-
from typing import Any, Sequence
34+
from typing import Any
3935

4036
__all__ = [
4137
"FilterExpression",
@@ -48,7 +44,6 @@
4844
"contains",
4945
"startswith",
5046
"endswith",
51-
"filter_in",
5247
"between",
5348
"is_null",
5449
"is_not_null",
@@ -174,22 +169,6 @@ def to_odata(self) -> str:
174169
return f"{self.func_name}({self.column}, {_format_value(self.value)})"
175170

176171

177-
class _InFilter(FilterExpression):
178-
"""In filter: ``column in (val1, val2, ...)``."""
179-
180-
__slots__ = ("column", "values")
181-
182-
def __init__(self, column: str, values: Sequence[Any]) -> None:
183-
if not values:
184-
raise ValueError("filter_in requires at least one value")
185-
self.column = column.lower()
186-
self.values = list(values)
187-
188-
def to_odata(self) -> str:
189-
formatted = ", ".join(_format_value(v) for v in self.values)
190-
return f"{self.column} in ({formatted})"
191-
192-
193172
class _AndFilter(FilterExpression):
194173
"""Logical AND: ``(left and right)``."""
195174

@@ -339,24 +318,6 @@ def endswith(column: str, value: str) -> FilterExpression:
339318
return _FunctionFilter("endswith", column, value)
340319

341320

342-
def filter_in(column: str, values: Sequence[Any]) -> FilterExpression:
343-
"""In filter: ``column in (val1, val2, ...)``.
344-
345-
Named ``filter_in`` because ``in`` is a Python keyword.
346-
347-
:param column: Column name (will be lowercased).
348-
:param values: Non-empty sequence of values.
349-
:return: A filter expression.
350-
:raises ValueError: If ``values`` is empty.
351-
352-
Example::
353-
354-
filter_in("statecode", [0, 1, 2]).to_odata()
355-
# "statecode in (0, 1, 2)"
356-
"""
357-
return _InFilter(column, values)
358-
359-
360321
def between(column: str, low: Any, high: Any) -> FilterExpression:
361322
"""Between filter: ``(column ge low and column le high)``.
362323

src/PowerPlatform/Dataverse/models/query_builder.py

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939

4040
from __future__ import annotations
4141

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

44-
from .filters import FilterExpression, _format_value
44+
from . import filters
4545

4646
__all__ = ["QueryBuilder"]
4747

@@ -75,7 +75,7 @@ def __init__(self, table: str) -> None:
7575
raise ValueError("table name is required")
7676
self.table = table
7777
self._select: List[str] = []
78-
self._filter_parts: List[Union[str, FilterExpression]] = []
78+
self._filter_parts: List[Union[str, filters.FilterExpression]] = []
7979
self._orderby: List[str] = []
8080
self._expand: List[str] = []
8181
self._top: Optional[int] = None
@@ -109,7 +109,7 @@ def filter_eq(self, column: str, value: Any) -> QueryBuilder:
109109
:param value: Value to compare against.
110110
:return: Self for method chaining.
111111
"""
112-
self._filter_parts.append(f"{column.lower()} eq {_format_value(value)}")
112+
self._filter_parts.append(filters.eq(column, value))
113113
return self
114114

115115
def filter_ne(self, column: str, value: Any) -> QueryBuilder:
@@ -119,7 +119,7 @@ def filter_ne(self, column: str, value: Any) -> QueryBuilder:
119119
:param value: Value to compare against.
120120
:return: Self for method chaining.
121121
"""
122-
self._filter_parts.append(f"{column.lower()} ne {_format_value(value)}")
122+
self._filter_parts.append(filters.ne(column, value))
123123
return self
124124

125125
def filter_gt(self, column: str, value: Any) -> QueryBuilder:
@@ -129,7 +129,7 @@ def filter_gt(self, column: str, value: Any) -> QueryBuilder:
129129
:param value: Value to compare against.
130130
:return: Self for method chaining.
131131
"""
132-
self._filter_parts.append(f"{column.lower()} gt {_format_value(value)}")
132+
self._filter_parts.append(filters.gt(column, value))
133133
return self
134134

135135
def filter_ge(self, column: str, value: Any) -> QueryBuilder:
@@ -139,7 +139,7 @@ def filter_ge(self, column: str, value: Any) -> QueryBuilder:
139139
:param value: Value to compare against.
140140
:return: Self for method chaining.
141141
"""
142-
self._filter_parts.append(f"{column.lower()} ge {_format_value(value)}")
142+
self._filter_parts.append(filters.ge(column, value))
143143
return self
144144

145145
def filter_lt(self, column: str, value: Any) -> QueryBuilder:
@@ -149,7 +149,7 @@ def filter_lt(self, column: str, value: Any) -> QueryBuilder:
149149
:param value: Value to compare against.
150150
:return: Self for method chaining.
151151
"""
152-
self._filter_parts.append(f"{column.lower()} lt {_format_value(value)}")
152+
self._filter_parts.append(filters.lt(column, value))
153153
return self
154154

155155
def filter_le(self, column: str, value: Any) -> QueryBuilder:
@@ -159,7 +159,7 @@ def filter_le(self, column: str, value: Any) -> QueryBuilder:
159159
:param value: Value to compare against.
160160
:return: Self for method chaining.
161161
"""
162-
self._filter_parts.append(f"{column.lower()} le {_format_value(value)}")
162+
self._filter_parts.append(filters.le(column, value))
163163
return self
164164

165165
# --------------------------------------------------------- filter: string functions
@@ -171,7 +171,7 @@ def filter_contains(self, column: str, value: str) -> QueryBuilder:
171171
:param value: Substring to search for.
172172
:return: Self for method chaining.
173173
"""
174-
self._filter_parts.append(f"contains({column.lower()}, {_format_value(value)})")
174+
self._filter_parts.append(filters.contains(column, value))
175175
return self
176176

177177
def filter_startswith(self, column: str, value: str) -> QueryBuilder:
@@ -181,7 +181,7 @@ def filter_startswith(self, column: str, value: str) -> QueryBuilder:
181181
:param value: Prefix to match.
182182
:return: Self for method chaining.
183183
"""
184-
self._filter_parts.append(f"startswith({column.lower()}, {_format_value(value)})")
184+
self._filter_parts.append(filters.startswith(column, value))
185185
return self
186186

187187
def filter_endswith(self, column: str, value: str) -> QueryBuilder:
@@ -191,7 +191,7 @@ def filter_endswith(self, column: str, value: str) -> QueryBuilder:
191191
:param value: Suffix to match.
192192
:return: Self for method chaining.
193193
"""
194-
self._filter_parts.append(f"endswith({column.lower()}, {_format_value(value)})")
194+
self._filter_parts.append(filters.endswith(column, value))
195195
return self
196196

197197
# --------------------------------------------------------- filter: null checks
@@ -202,7 +202,7 @@ def filter_null(self, column: str) -> QueryBuilder:
202202
:param column: Column name (will be lowercased).
203203
:return: Self for method chaining.
204204
"""
205-
self._filter_parts.append(f"{column.lower()} eq null")
205+
self._filter_parts.append(filters.is_null(column))
206206
return self
207207

208208
def filter_not_null(self, column: str) -> QueryBuilder:
@@ -211,30 +211,11 @@ def filter_not_null(self, column: str) -> QueryBuilder:
211211
:param column: Column name (will be lowercased).
212212
:return: Self for method chaining.
213213
"""
214-
self._filter_parts.append(f"{column.lower()} ne null")
214+
self._filter_parts.append(filters.is_not_null(column))
215215
return self
216216

217217
# --------------------------------------------------------- filter: special
218218

219-
def filter_in(self, column: str, values: Sequence[Any]) -> QueryBuilder:
220-
"""Add an ``in`` filter: ``column in (val1, val2, ...)``.
221-
222-
:param column: Column name (will be lowercased).
223-
:param values: Non-empty list of values for the ``in`` clause.
224-
:return: Self for method chaining.
225-
:raises ValueError: If ``values`` is empty.
226-
227-
Example::
228-
229-
query = QueryBuilder("account").filter_in("statecode", [0, 1, 2])
230-
# Produces: statecode in (0, 1, 2)
231-
"""
232-
if not values:
233-
raise ValueError("filter_in requires at least one value")
234-
formatted = ", ".join(_format_value(v) for v in values)
235-
self._filter_parts.append(f"{column.lower()} in ({formatted})")
236-
return self
237-
238219
def filter_between(self, column: str, low: Any, high: Any) -> QueryBuilder:
239220
"""Add a between filter: ``(column ge low and column le high)``.
240221
@@ -248,8 +229,7 @@ def filter_between(self, column: str, low: Any, high: Any) -> QueryBuilder:
248229
query = QueryBuilder("account").filter_between("revenue", 100000, 500000)
249230
# Produces: (revenue ge 100000 and revenue le 500000)
250231
"""
251-
col = column.lower()
252-
self._filter_parts.append(f"({col} ge {_format_value(low)} and {col} le {_format_value(high)})")
232+
self._filter_parts.append(filters.between(column, low, high))
253233
return self
254234

255235
def filter_raw(self, filter_string: str) -> QueryBuilder:
@@ -267,12 +247,12 @@ def filter_raw(self, filter_string: str) -> QueryBuilder:
267247
"(statecode eq 0 or statecode eq 1)"
268248
)
269249
"""
270-
self._filter_parts.append(filter_string)
250+
self._filter_parts.append(filters.raw(filter_string))
271251
return self
272252

273253
# ------------------------------------------------------ filter: expression tree
274254

275-
def where(self, expression: FilterExpression) -> QueryBuilder:
255+
def where(self, expression: filters.FilterExpression) -> QueryBuilder:
276256
"""Add a composable filter expression.
277257
278258
Accepts a :class:`~PowerPlatform.Dataverse.models.filters.FilterExpression`
@@ -295,7 +275,7 @@ def where(self, expression: FilterExpression) -> QueryBuilder:
295275
.where((eq("statecode", 0) | eq("statecode", 1))
296276
& gt("revenue", 100000)))
297277
"""
298-
if not isinstance(expression, FilterExpression):
278+
if not isinstance(expression, filters.FilterExpression):
299279
raise TypeError(f"where() requires a FilterExpression, got {type(expression).__name__}")
300280
self._filter_parts.append(expression)
301281
return self
@@ -376,7 +356,7 @@ def build(self) -> dict:
376356
if self._filter_parts:
377357
parts: List[str] = []
378358
for part in self._filter_parts:
379-
if isinstance(part, FilterExpression):
359+
if isinstance(part, filters.FilterExpression):
380360
parts.append(part.to_odata())
381361
else:
382362
parts.append(part)

0 commit comments

Comments
 (0)