Skip to content

Commit 356004b

Browse files
Abel Milashclaude
andcommitted
Refactor: add _BatchContext Protocol and extract _QueryBuilderBase
- Add _BatchContext Protocol to _batch_base.py; re-type BatchRecordOperations, BatchTableOperations, BatchQueryOperations, BatchDataFrameOperations __init__ from BatchRequest to _BatchContext — removes type: ignore on AsyncBatchRequest - Extract _QueryBuilderBase from QueryBuilder with all fluent methods and build(); QueryBuilder inherits base and keeps execute/execute_pages/to_dataframe; AsyncQueryBuilder will inherit base directly, eliminating deprecated sync surface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8039dd0 commit 356004b

3 files changed

Lines changed: 70 additions & 36 deletions

File tree

src/PowerPlatform/Dataverse/data/_batch_base.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import re
1313
import uuid
1414
from dataclasses import dataclass, field
15-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
15+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Tuple, Union
1616

1717
from ..core.errors import HttpError, ValidationError
1818
from ..core._error_codes import _http_subcode
@@ -235,6 +235,23 @@ class _ChangeSetBatchItem:
235235
requests: List[_RawRequest]
236236

237237

238+
# ---------------------------------------------------------------------------
239+
# Shared interface for batch operation namespaces
240+
# ---------------------------------------------------------------------------
241+
242+
243+
class _BatchContext(Protocol):
244+
"""Structural interface required by batch operation namespaces.
245+
246+
Both :class:`~PowerPlatform.Dataverse.operations.batch.BatchRequest` and
247+
:class:`~PowerPlatform.Dataverse.aio.operations.async_batch.AsyncBatchRequest`
248+
satisfy this protocol — no explicit inheritance needed on either class.
249+
"""
250+
251+
_items: List[Any]
252+
records: Any # used by BatchDataFrameOperations to delegate create/update/delete
253+
254+
238255
# ---------------------------------------------------------------------------
239256
# Multipart parsing helpers
240257
# ---------------------------------------------------------------------------

src/PowerPlatform/Dataverse/models/query_builder.py

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from __future__ import annotations
5151

5252
import warnings
53-
from typing import Any, Dict, Iterator, List, Optional, TypedDict, Union
53+
from typing import Any, Iterator, List, Optional, TypeVar, TypedDict, Union
5454

5555
import pandas as pd
5656

@@ -62,6 +62,8 @@
6262
# Sentinel for detecting when by_page is explicitly passed to execute()
6363
_BY_PAGE_UNSET = object()
6464

65+
_QB = TypeVar("_QB", bound="_QueryBuilderBase")
66+
6567

6668
class QueryParams(TypedDict, total=False):
6769
"""Typed dictionary returned by :meth:`QueryBuilder.build`.
@@ -171,29 +173,17 @@ def to_odata(self) -> str:
171173
return self.relation
172174

173175

174-
class QueryBuilder:
175-
"""Fluent interface for building OData queries.
176-
177-
Provides method chaining for constructing complex queries with
178-
composable filter expressions. Can be used standalone (via :meth:`build`)
179-
or bound to a client (via :meth:`execute`).
180-
181-
:param table: Table schema name to query.
182-
:type table: str
183-
:raises ValueError: If ``table`` is empty.
184-
185-
Example:
186-
Standalone query construction::
176+
class _QueryBuilderBase:
177+
"""Pure fluent interface for building OData queries — no I/O.
187178
188-
from PowerPlatform.Dataverse.models.filters import col
179+
Holds all query state and chaining methods (``select``, ``where``,
180+
``order_by``, ``top``, ``page_size``, ``count``, ``expand``,
181+
``include_annotations``, ``include_formatted_values``) and
182+
:meth:`build`.
189183
190-
query = (QueryBuilder("account")
191-
.select("name")
192-
.where(col("statecode") == 0)
193-
.top(10))
194-
params = query.build()
195-
# {"table": "account", "select": ["name"],
196-
# "filter": "statecode eq 0", "top": 10}
184+
Subclasses add execution: :class:`QueryBuilder` for sync clients,
185+
:class:`~PowerPlatform.Dataverse.aio.models.async_query_builder.AsyncQueryBuilder`
186+
for async clients.
197187
"""
198188

199189
def __init__(self, table: str) -> None:
@@ -213,7 +203,7 @@ def __init__(self, table: str) -> None:
213203

214204
# ----------------------------------------------------------------- select
215205

216-
def select(self, *columns: str) -> QueryBuilder:
206+
def select(self: _QB, *columns: str) -> _QB:
217207
"""Select specific columns to retrieve.
218208
219209
Column names are passed as-is; the OData layer lowercases them
@@ -231,7 +221,7 @@ def select(self, *columns: str) -> QueryBuilder:
231221

232222
# ------------------------------------------------------ filter: expression tree
233223

234-
def where(self, expression: filters.FilterExpression) -> QueryBuilder:
224+
def where(self: _QB, expression: filters.FilterExpression) -> _QB:
235225
"""Add a composable filter expression.
236226
237227
Accepts a :class:`~PowerPlatform.Dataverse.models.filters.FilterExpression`
@@ -260,7 +250,7 @@ def where(self, expression: filters.FilterExpression) -> QueryBuilder:
260250

261251
# --------------------------------------------------------------- ordering
262252

263-
def order_by(self, column: str, descending: bool = False) -> QueryBuilder:
253+
def order_by(self: _QB, column: str, descending: bool = False) -> _QB:
264254
"""Add sorting order.
265255
266256
Can be called multiple times for multi-column sorting.
@@ -275,7 +265,7 @@ def order_by(self, column: str, descending: bool = False) -> QueryBuilder:
275265

276266
# --------------------------------------------------------------- pagination
277267

278-
def top(self, count: int) -> QueryBuilder:
268+
def top(self: _QB, count: int) -> _QB:
279269
"""Limit the total number of results.
280270
281271
:param count: Maximum number of records to return (must be >= 1).
@@ -287,7 +277,7 @@ def top(self, count: int) -> QueryBuilder:
287277
self._top = count
288278
return self
289279

290-
def page_size(self, size: int) -> QueryBuilder:
280+
def page_size(self: _QB, size: int) -> _QB:
291281
"""Set the number of records per page.
292282
293283
Controls how many records are returned in each page/batch
@@ -302,7 +292,7 @@ def page_size(self, size: int) -> QueryBuilder:
302292
self._page_size = size
303293
return self
304294

305-
def count(self) -> QueryBuilder:
295+
def count(self: _QB) -> _QB:
306296
"""Request a count of matching records in the response.
307297
308298
Adds ``$count=true`` to the query, causing the server to include
@@ -321,7 +311,7 @@ def count(self) -> QueryBuilder:
321311
self._count = True
322312
return self
323313

324-
def include_formatted_values(self) -> QueryBuilder:
314+
def include_formatted_values(self: _QB) -> _QB:
325315
"""Request formatted values in the response.
326316
327317
Adds ``Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"``
@@ -351,7 +341,7 @@ def include_formatted_values(self) -> QueryBuilder:
351341
self._include_annotations = "OData.Community.Display.V1.FormattedValue"
352342
return self
353343

354-
def include_annotations(self, annotation: str = "*") -> QueryBuilder:
344+
def include_annotations(self: _QB, annotation: str = "*") -> _QB:
355345
"""Request specific OData annotations in the response.
356346
357347
Sets the ``Prefer: odata.include-annotations`` header. Use ``"*"``
@@ -379,7 +369,7 @@ def include_annotations(self, annotation: str = "*") -> QueryBuilder:
379369

380370
# --------------------------------------------------------------- expand
381371

382-
def expand(self, *relations: Union[str, ExpandOption]) -> QueryBuilder:
372+
def expand(self: _QB, *relations: Union[str, ExpandOption]) -> _QB:
383373
"""Expand navigation properties.
384374
385375
Accepts plain navigation property names (case-sensitive, passed
@@ -448,6 +438,32 @@ def build(self) -> QueryParams:
448438
params["include_annotations"] = self._include_annotations
449439
return params
450440

441+
442+
class QueryBuilder(_QueryBuilderBase):
443+
"""Fluent interface for building and executing OData queries against a sync client.
444+
445+
Provides method chaining for constructing complex queries with
446+
composable filter expressions. Can be used standalone (via :meth:`build`)
447+
or bound to a client (via :meth:`execute`).
448+
449+
:param table: Table schema name to query.
450+
:type table: str
451+
:raises ValueError: If ``table`` is empty.
452+
453+
Example:
454+
Standalone query construction::
455+
456+
from PowerPlatform.Dataverse.models.filters import col
457+
458+
query = (QueryBuilder("account")
459+
.select("name")
460+
.where(col("statecode") == 0)
461+
.top(10))
462+
params = query.build()
463+
# {"table": "account", "select": ["name"],
464+
# "filter": "statecode eq 0", "top": 10}
465+
"""
466+
451467
# --------------------------------------------------------------- execute
452468

453469
def execute(self, *, by_page=_BY_PAGE_UNSET) -> Union[QueryResult, Iterator[QueryResult]]:

src/PowerPlatform/Dataverse/operations/batch.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from ..core.errors import ValidationError
1414
from ..core._error_codes import VALIDATION_SQL_EMPTY
15+
from ..data._batch_base import _BatchContext
1516
from ..data._batch import (
1617
_BatchClient,
1718
_ChangeSet,
@@ -180,7 +181,7 @@ class BatchRecordOperations:
180181
Do not instantiate directly; use ``batch.records``.
181182
"""
182183

183-
def __init__(self, batch: "BatchRequest") -> None:
184+
def __init__(self, batch: "_BatchContext") -> None:
184185
self._batch = batch
185186

186187
def create(
@@ -482,7 +483,7 @@ class BatchTableOperations:
482483
Do not instantiate directly; use ``batch.tables``.
483484
"""
484485

485-
def __init__(self, batch: "BatchRequest") -> None:
486+
def __init__(self, batch: "_BatchContext") -> None:
486487
self._batch = batch
487488

488489
def create(
@@ -720,7 +721,7 @@ class BatchQueryOperations:
720721
Do not instantiate directly; use ``batch.query``.
721722
"""
722723

723-
def __init__(self, batch: "BatchRequest") -> None:
724+
def __init__(self, batch: "_BatchContext") -> None:
724725
self._batch = batch
725726

726727
def sql(self, sql: str) -> None:
@@ -774,7 +775,7 @@ class BatchDataFrameOperations:
774775
result = batch.execute()
775776
"""
776777

777-
def __init__(self, batch: "BatchRequest") -> None:
778+
def __init__(self, batch: "_BatchContext") -> None:
778779
self._batch = batch
779780

780781
def create(self, table: str, records: pd.DataFrame) -> None:

0 commit comments

Comments
 (0)