Skip to content

Commit 4843654

Browse files
tpellissierclaude
andcommitted
Add operation namespaces (client.records, client.query, client.tables)
Reorganize SDK public API into three operation namespaces per the SDK Redesign Summary. New namespace methods use cleaner signatures (keyword-only params, @overload for single/bulk, renamed table params) while all existing flat methods (client.create, client.get, etc.) continue to work unchanged with deprecation warnings that point to the new equivalents. Key changes: - records.create() returns str for single, list[str] for bulk - query.get() handles paginated multi-record queries - tables.create() uses solution= and primary_column= keyword args - 40 new unit tests (75 total, all passing) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2042ad4 commit 4843654

10 files changed

Lines changed: 1495 additions & 88 deletions

File tree

src/PowerPlatform/Dataverse/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,15 @@
22
# Licensed under the MIT license.
33

44
from .__version__ import __version__
5+
from .client import DataverseClient
6+
from .operations.records import RecordOperations
7+
from .operations.query import QueryOperations
8+
from .operations.tables import TableOperations
59

6-
__all__ = ["__version__"]
10+
__all__ = [
11+
"__version__",
12+
"DataverseClient",
13+
"RecordOperations",
14+
"QueryOperations",
15+
"TableOperations",
16+
]

src/PowerPlatform/Dataverse/client.py

Lines changed: 112 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
from typing import Any, Dict, Optional, Union, List, Iterable, Iterator
77
from contextlib import contextmanager
88

9+
import warnings
10+
911
from azure.core.credentials import TokenCredential
1012

1113
from .core._auth import _AuthManager
1214
from .core.config import DataverseConfig
1315
from .data._odata import _ODataClient
16+
from .operations.records import RecordOperations
17+
from .operations.query import QueryOperations
18+
from .operations.tables import TableOperations
1419

1520

1621
class DataverseClient:
@@ -82,6 +87,11 @@ def __init__(
8287
self._config = config or DataverseConfig.from_env()
8388
self._odata: Optional[_ODataClient] = None
8489

90+
# Operation namespaces
91+
self.records = RecordOperations(self)
92+
self.query = QueryOperations(self)
93+
self.tables = TableOperations(self)
94+
8595
def _get_odata(self) -> _ODataClient:
8696
"""
8797
Get or create the internal OData client instance.
@@ -110,6 +120,9 @@ def _scoped_odata(self) -> Iterator[_ODataClient]:
110120
# ---------------- Unified CRUD: create/update/delete ----------------
111121
def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[str]:
112122
"""
123+
.. deprecated::
124+
Use :meth:`client.records.create()` instead.
125+
113126
Create one or more records by table name.
114127
115128
:param table_schema_name: Schema name of the table (e.g. ``"account"``, ``"contact"``, or ``"new_MyTestTable"``).
@@ -140,25 +153,23 @@ def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dic
140153
ids = client.create("account", records)
141154
print(f"Created {len(ids)} accounts")
142155
"""
143-
with self._scoped_odata() as od:
144-
entity_set = od._entity_set_from_schema_name(table_schema_name)
145-
if isinstance(records, dict):
146-
rid = od._create(entity_set, table_schema_name, records)
147-
# _create returns str on single input
148-
if not isinstance(rid, str):
149-
raise TypeError("_create (single) did not return GUID string")
150-
return [rid]
151-
if isinstance(records, list):
152-
ids = od._create_multiple(entity_set, table_schema_name, records)
153-
if not isinstance(ids, list) or not all(isinstance(x, str) for x in ids):
154-
raise TypeError("_create (multi) did not return list[str]")
155-
return ids
156-
raise TypeError("records must be dict or list[dict]")
156+
warnings.warn(
157+
"client.create() is deprecated. Use client.records.create() instead.",
158+
DeprecationWarning, stacklevel=2,
159+
)
160+
result = self.records.create(table_schema_name, records)
161+
# Old API always returned list[str], new returns str for single
162+
if isinstance(records, dict):
163+
return [result]
164+
return result
157165

158166
def update(
159167
self, table_schema_name: str, ids: Union[str, List[str]], changes: Union[Dict[str, Any], List[Dict[str, Any]]]
160168
) -> None:
161169
"""
170+
.. deprecated::
171+
Use :meth:`client.records.update()` instead.
172+
162173
Update one or more records.
163174
164175
This method supports three usage patterns:
@@ -200,16 +211,11 @@ def update(
200211
]
201212
client.update("account", ids, changes)
202213
"""
203-
with self._scoped_odata() as od:
204-
if isinstance(ids, str):
205-
if not isinstance(changes, dict):
206-
raise TypeError("For single id, changes must be a dict")
207-
od._update(table_schema_name, ids, changes) # discard representation
208-
return None
209-
if not isinstance(ids, list):
210-
raise TypeError("ids must be str or list[str]")
211-
od._update_by_ids(table_schema_name, ids, changes)
212-
return None
214+
warnings.warn(
215+
"client.update() is deprecated. Use client.records.update() instead.",
216+
DeprecationWarning, stacklevel=2,
217+
)
218+
self.records.update(table_schema_name, ids, changes)
213219

214220
def delete(
215221
self,
@@ -218,6 +224,9 @@ def delete(
218224
use_bulk_delete: bool = True,
219225
) -> Optional[str]:
220226
"""
227+
.. deprecated::
228+
Use :meth:`client.records.delete()` instead.
229+
221230
Delete one or more records by GUID.
222231
223232
:param table_schema_name: Schema name of the table (e.g. ``"account"`` or ``"new_MyTestTable"``).
@@ -243,21 +252,11 @@ def delete(
243252
244253
job_id = client.delete("account", [id1, id2, id3])
245254
"""
246-
with self._scoped_odata() as od:
247-
if isinstance(ids, str):
248-
od._delete(table_schema_name, ids)
249-
return None
250-
if not isinstance(ids, list):
251-
raise TypeError("ids must be str or list[str]")
252-
if not ids:
253-
return None
254-
if not all(isinstance(rid, str) for rid in ids):
255-
raise TypeError("ids must contain string GUIDs")
256-
if use_bulk_delete:
257-
return od._delete_multiple(table_schema_name, ids)
258-
for rid in ids:
259-
od._delete(table_schema_name, rid)
260-
return None
255+
warnings.warn(
256+
"client.delete() is deprecated. Use client.records.delete() instead.",
257+
DeprecationWarning, stacklevel=2,
258+
)
259+
return self.records.delete(table_schema_name, ids, use_bulk_delete=use_bulk_delete)
261260

262261
def get(
263262
self,
@@ -271,6 +270,9 @@ def get(
271270
page_size: Optional[int] = None,
272271
) -> Union[Dict[str, Any], Iterable[List[Dict[str, Any]]]]:
273272
"""
273+
.. deprecated::
274+
Use :meth:`client.records.get()` or :meth:`client.query.get()` instead.
275+
274276
Fetch a single record by ID or query multiple records.
275277
276278
When ``record_id`` is provided, returns a single record dictionary.
@@ -336,33 +338,24 @@ def get(
336338
):
337339
print(f"Batch size: {len(batch)}")
338340
"""
341+
warnings.warn(
342+
"client.get() is deprecated. Use client.records.get() or client.query.get() instead.",
343+
DeprecationWarning, stacklevel=2,
344+
)
339345
if record_id is not None:
340-
if not isinstance(record_id, str):
341-
raise TypeError("record_id must be str")
342-
with self._scoped_odata() as od:
343-
return od._get(
344-
table_schema_name,
345-
record_id,
346-
select=select,
347-
)
348-
349-
def _paged() -> Iterable[List[Dict[str, Any]]]:
350-
with self._scoped_odata() as od:
351-
yield from od._get_multiple(
352-
table_schema_name,
353-
select=select,
354-
filter=filter,
355-
orderby=orderby,
356-
top=top,
357-
expand=expand,
358-
page_size=page_size,
359-
)
360-
361-
return _paged()
346+
return self.records.get(table_schema_name, record_id, select=select, expand=expand)
347+
else:
348+
return self.query.get(
349+
table_schema_name, select=select, filter=filter,
350+
orderby=orderby, top=top, expand=expand, page_size=page_size,
351+
)
362352

363353
# SQL via Web API sql parameter
364354
def query_sql(self, sql: str):
365355
"""
356+
.. deprecated::
357+
Use :meth:`client.query.sql()` instead.
358+
366359
Execute a read-only SQL query using the Dataverse Web API ``?sql`` capability.
367360
368361
The SQL query must follow the supported subset: a single SELECT statement with
@@ -394,12 +387,18 @@ def query_sql(self, sql: str):
394387
sql = "SELECT a.name, a.telephone1 FROM account AS a WHERE a.statecode = 0"
395388
results = client.query_sql(sql)
396389
"""
397-
with self._scoped_odata() as od:
398-
return od._query_sql(sql)
390+
warnings.warn(
391+
"client.query_sql() is deprecated. Use client.query.sql() instead.",
392+
DeprecationWarning, stacklevel=2,
393+
)
394+
return self.query.sql(sql)
399395

400396
# Table metadata helpers
401397
def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
402398
"""
399+
.. deprecated::
400+
Use :meth:`client.tables.get()` instead.
401+
403402
Get basic metadata for a table if it exists.
404403
405404
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"`` or ``"account"``).
@@ -418,8 +417,11 @@ def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
418417
print(f"Logical name: {info['table_logical_name']}")
419418
print(f"Entity set: {info['entity_set_name']}")
420419
"""
421-
with self._scoped_odata() as od:
422-
return od._get_table_info(table_schema_name)
420+
warnings.warn(
421+
"client.get_table_info() is deprecated. Use client.tables.get() instead.",
422+
DeprecationWarning, stacklevel=2,
423+
)
424+
return self.tables.get(table_schema_name)
423425

424426
def create_table(
425427
self,
@@ -429,6 +431,9 @@ def create_table(
429431
primary_column_schema_name: Optional[str] = None,
430432
) -> Dict[str, Any]:
431433
"""
434+
.. deprecated::
435+
Use :meth:`client.tables.create()` instead.
436+
432437
Create a simple custom table with specified columns.
433438
434439
:param table_schema_name: Schema name of the table with customization prefix value (e.g. ``"new_MyTestTable"``).
@@ -489,16 +494,21 @@ class ItemStatus(IntEnum):
489494
primary_column_schema_name="new_ProductName"
490495
)
491496
"""
492-
with self._scoped_odata() as od:
493-
return od._create_table(
494-
table_schema_name,
495-
columns,
496-
solution_unique_name,
497-
primary_column_schema_name,
498-
)
497+
warnings.warn(
498+
"client.create_table() is deprecated. Use client.tables.create() instead.",
499+
DeprecationWarning, stacklevel=2,
500+
)
501+
return self.tables.create(
502+
table_schema_name, columns,
503+
solution=solution_unique_name,
504+
primary_column=primary_column_schema_name,
505+
)
499506

500507
def delete_table(self, table_schema_name: str) -> None:
501508
"""
509+
.. deprecated::
510+
Use :meth:`client.tables.delete()` instead.
511+
502512
Delete a custom table by name.
503513
504514
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"`` or ``"account"``).
@@ -515,11 +525,17 @@ def delete_table(self, table_schema_name: str) -> None:
515525
516526
client.delete_table("new_MyTestTable")
517527
"""
518-
with self._scoped_odata() as od:
519-
od._delete_table(table_schema_name)
528+
warnings.warn(
529+
"client.delete_table() is deprecated. Use client.tables.delete() instead.",
530+
DeprecationWarning, stacklevel=2,
531+
)
532+
self.tables.delete(table_schema_name)
520533

521534
def list_tables(self) -> list[dict[str, Any]]:
522535
"""
536+
.. deprecated::
537+
Use :meth:`client.tables.list()` instead.
538+
523539
List all non-private tables in the Dataverse environment.
524540
525541
:return: List of EntityDefinition metadata dictionaries.
@@ -532,15 +548,21 @@ def list_tables(self) -> list[dict[str, Any]]:
532548
for table in tables:
533549
print(table["LogicalName"])
534550
"""
535-
with self._scoped_odata() as od:
536-
return od._list_tables()
551+
warnings.warn(
552+
"client.list_tables() is deprecated. Use client.tables.list() instead.",
553+
DeprecationWarning, stacklevel=2,
554+
)
555+
return self.tables.list()
537556

538557
def create_columns(
539558
self,
540559
table_schema_name: str,
541560
columns: Dict[str, Any],
542561
) -> List[str]:
543562
"""
563+
.. deprecated::
564+
Use :meth:`client.tables.add_columns()` instead.
565+
544566
Create one or more columns on an existing table using a schema-style mapping.
545567
546568
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"``).
@@ -564,18 +586,21 @@ def create_columns(
564586
)
565587
print(created) # ['new_Scratch', 'new_Flags', 'new_Document']
566588
"""
567-
with self._scoped_odata() as od:
568-
return od._create_columns(
569-
table_schema_name,
570-
columns,
571-
)
589+
warnings.warn(
590+
"client.create_columns() is deprecated. Use client.tables.add_columns() instead.",
591+
DeprecationWarning, stacklevel=2,
592+
)
593+
return self.tables.add_columns(table_schema_name, columns)
572594

573595
def delete_columns(
574596
self,
575597
table_schema_name: str,
576598
columns: Union[str, List[str]],
577599
) -> List[str]:
578600
"""
601+
.. deprecated::
602+
Use :meth:`client.tables.remove_columns()` instead.
603+
579604
Delete one or more columns from a table.
580605
581606
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"``).
@@ -593,11 +618,11 @@ def delete_columns(
593618
)
594619
print(removed) # ['new_Scratch', 'new_Flags']
595620
"""
596-
with self._scoped_odata() as od:
597-
return od._delete_columns(
598-
table_schema_name,
599-
columns,
600-
)
621+
warnings.warn(
622+
"client.delete_columns() is deprecated. Use client.tables.remove_columns() instead.",
623+
DeprecationWarning, stacklevel=2,
624+
)
625+
return self.tables.remove_columns(table_schema_name, columns)
601626

602627
# File upload
603628
def upload_file(
@@ -699,4 +724,4 @@ def flush_cache(self, kind) -> int:
699724
return od._flush_cache(kind)
700725

701726

702-
__all__ = ["DataverseClient"]
727+
__all__ = ["DataverseClient", "RecordOperations", "QueryOperations", "TableOperations"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""Operation namespaces for the Dataverse SDK."""
5+
6+
from .records import RecordOperations
7+
from .query import QueryOperations
8+
from .tables import TableOperations
9+
10+
__all__ = ["RecordOperations", "QueryOperations", "TableOperations"]

0 commit comments

Comments
 (0)