Skip to content

Commit 5ce9cbe

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 5ce9cbe

10 files changed

Lines changed: 1497 additions & 87 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: 128 additions & 86 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,24 @@ 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,
159+
stacklevel=2,
160+
)
161+
result = self.records.create(table_schema_name, records)
162+
# Old API always returned list[str], new returns str for single
163+
if isinstance(records, dict):
164+
return [result]
165+
return result
157166

158167
def update(
159168
self, table_schema_name: str, ids: Union[str, List[str]], changes: Union[Dict[str, Any], List[Dict[str, Any]]]
160169
) -> None:
161170
"""
171+
.. deprecated::
172+
Use :meth:`client.records.update()` instead.
173+
162174
Update one or more records.
163175
164176
This method supports three usage patterns:
@@ -200,16 +212,12 @@ def update(
200212
]
201213
client.update("account", ids, changes)
202214
"""
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
215+
warnings.warn(
216+
"client.update() is deprecated. Use client.records.update() instead.",
217+
DeprecationWarning,
218+
stacklevel=2,
219+
)
220+
self.records.update(table_schema_name, ids, changes)
213221

214222
def delete(
215223
self,
@@ -218,6 +226,9 @@ def delete(
218226
use_bulk_delete: bool = True,
219227
) -> Optional[str]:
220228
"""
229+
.. deprecated::
230+
Use :meth:`client.records.delete()` instead.
231+
221232
Delete one or more records by GUID.
222233
223234
:param table_schema_name: Schema name of the table (e.g. ``"account"`` or ``"new_MyTestTable"``).
@@ -243,21 +254,12 @@ def delete(
243254
244255
job_id = client.delete("account", [id1, id2, id3])
245256
"""
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
257+
warnings.warn(
258+
"client.delete() is deprecated. Use client.records.delete() instead.",
259+
DeprecationWarning,
260+
stacklevel=2,
261+
)
262+
return self.records.delete(table_schema_name, ids, use_bulk_delete=use_bulk_delete)
261263

262264
def get(
263265
self,
@@ -271,6 +273,9 @@ def get(
271273
page_size: Optional[int] = None,
272274
) -> Union[Dict[str, Any], Iterable[List[Dict[str, Any]]]]:
273275
"""
276+
.. deprecated::
277+
Use :meth:`client.records.get()` or :meth:`client.query.get()` instead.
278+
274279
Fetch a single record by ID or query multiple records.
275280
276281
When ``record_id`` is provided, returns a single record dictionary.
@@ -336,33 +341,30 @@ def get(
336341
):
337342
print(f"Batch size: {len(batch)}")
338343
"""
344+
warnings.warn(
345+
"client.get() is deprecated. Use client.records.get() or client.query.get() instead.",
346+
DeprecationWarning,
347+
stacklevel=2,
348+
)
339349
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()
350+
return self.records.get(table_schema_name, record_id, select=select, expand=expand)
351+
else:
352+
return self.query.get(
353+
table_schema_name,
354+
select=select,
355+
filter=filter,
356+
orderby=orderby,
357+
top=top,
358+
expand=expand,
359+
page_size=page_size,
360+
)
362361

363362
# SQL via Web API sql parameter
364363
def query_sql(self, sql: str):
365364
"""
365+
.. deprecated::
366+
Use :meth:`client.query.sql()` instead.
367+
366368
Execute a read-only SQL query using the Dataverse Web API ``?sql`` capability.
367369
368370
The SQL query must follow the supported subset: a single SELECT statement with
@@ -394,12 +396,19 @@ def query_sql(self, sql: str):
394396
sql = "SELECT a.name, a.telephone1 FROM account AS a WHERE a.statecode = 0"
395397
results = client.query_sql(sql)
396398
"""
397-
with self._scoped_odata() as od:
398-
return od._query_sql(sql)
399+
warnings.warn(
400+
"client.query_sql() is deprecated. Use client.query.sql() instead.",
401+
DeprecationWarning,
402+
stacklevel=2,
403+
)
404+
return self.query.sql(sql)
399405

400406
# Table metadata helpers
401407
def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
402408
"""
409+
.. deprecated::
410+
Use :meth:`client.tables.get()` instead.
411+
403412
Get basic metadata for a table if it exists.
404413
405414
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"`` or ``"account"``).
@@ -418,8 +427,12 @@ def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
418427
print(f"Logical name: {info['table_logical_name']}")
419428
print(f"Entity set: {info['entity_set_name']}")
420429
"""
421-
with self._scoped_odata() as od:
422-
return od._get_table_info(table_schema_name)
430+
warnings.warn(
431+
"client.get_table_info() is deprecated. Use client.tables.get() instead.",
432+
DeprecationWarning,
433+
stacklevel=2,
434+
)
435+
return self.tables.get(table_schema_name)
423436

424437
def create_table(
425438
self,
@@ -429,6 +442,9 @@ def create_table(
429442
primary_column_schema_name: Optional[str] = None,
430443
) -> Dict[str, Any]:
431444
"""
445+
.. deprecated::
446+
Use :meth:`client.tables.create()` instead.
447+
432448
Create a simple custom table with specified columns.
433449
434450
:param table_schema_name: Schema name of the table with customization prefix value (e.g. ``"new_MyTestTable"``).
@@ -489,16 +505,23 @@ class ItemStatus(IntEnum):
489505
primary_column_schema_name="new_ProductName"
490506
)
491507
"""
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-
)
508+
warnings.warn(
509+
"client.create_table() is deprecated. Use client.tables.create() instead.",
510+
DeprecationWarning,
511+
stacklevel=2,
512+
)
513+
return self.tables.create(
514+
table_schema_name,
515+
columns,
516+
solution=solution_unique_name,
517+
primary_column=primary_column_schema_name,
518+
)
499519

500520
def delete_table(self, table_schema_name: str) -> None:
501521
"""
522+
.. deprecated::
523+
Use :meth:`client.tables.delete()` instead.
524+
502525
Delete a custom table by name.
503526
504527
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"`` or ``"account"``).
@@ -515,11 +538,18 @@ def delete_table(self, table_schema_name: str) -> None:
515538
516539
client.delete_table("new_MyTestTable")
517540
"""
518-
with self._scoped_odata() as od:
519-
od._delete_table(table_schema_name)
541+
warnings.warn(
542+
"client.delete_table() is deprecated. Use client.tables.delete() instead.",
543+
DeprecationWarning,
544+
stacklevel=2,
545+
)
546+
self.tables.delete(table_schema_name)
520547

521548
def list_tables(self) -> list[dict[str, Any]]:
522549
"""
550+
.. deprecated::
551+
Use :meth:`client.tables.list()` instead.
552+
523553
List all non-private tables in the Dataverse environment.
524554
525555
:return: List of EntityDefinition metadata dictionaries.
@@ -532,15 +562,22 @@ def list_tables(self) -> list[dict[str, Any]]:
532562
for table in tables:
533563
print(table["LogicalName"])
534564
"""
535-
with self._scoped_odata() as od:
536-
return od._list_tables()
565+
warnings.warn(
566+
"client.list_tables() is deprecated. Use client.tables.list() instead.",
567+
DeprecationWarning,
568+
stacklevel=2,
569+
)
570+
return self.tables.list()
537571

538572
def create_columns(
539573
self,
540574
table_schema_name: str,
541575
columns: Dict[str, Any],
542576
) -> List[str]:
543577
"""
578+
.. deprecated::
579+
Use :meth:`client.tables.add_columns()` instead.
580+
544581
Create one or more columns on an existing table using a schema-style mapping.
545582
546583
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"``).
@@ -564,18 +601,22 @@ def create_columns(
564601
)
565602
print(created) # ['new_Scratch', 'new_Flags', 'new_Document']
566603
"""
567-
with self._scoped_odata() as od:
568-
return od._create_columns(
569-
table_schema_name,
570-
columns,
571-
)
604+
warnings.warn(
605+
"client.create_columns() is deprecated. Use client.tables.add_columns() instead.",
606+
DeprecationWarning,
607+
stacklevel=2,
608+
)
609+
return self.tables.add_columns(table_schema_name, columns)
572610

573611
def delete_columns(
574612
self,
575613
table_schema_name: str,
576614
columns: Union[str, List[str]],
577615
) -> List[str]:
578616
"""
617+
.. deprecated::
618+
Use :meth:`client.tables.remove_columns()` instead.
619+
579620
Delete one or more columns from a table.
580621
581622
:param table_schema_name: Schema name of the table (e.g. ``"new_MyTestTable"``).
@@ -593,11 +634,12 @@ def delete_columns(
593634
)
594635
print(removed) # ['new_Scratch', 'new_Flags']
595636
"""
596-
with self._scoped_odata() as od:
597-
return od._delete_columns(
598-
table_schema_name,
599-
columns,
600-
)
637+
warnings.warn(
638+
"client.delete_columns() is deprecated. Use client.tables.remove_columns() instead.",
639+
DeprecationWarning,
640+
stacklevel=2,
641+
)
642+
return self.tables.remove_columns(table_schema_name, columns)
601643

602644
# File upload
603645
def upload_file(

0 commit comments

Comments
 (0)