Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 104 additions & 83 deletions src/PowerPlatform/Dataverse/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

from __future__ import annotations

from typing import Any, Dict, Optional, Union, List, Iterable
from typing import Any, Dict, Optional, Union, List, Iterable, Iterator
from contextlib import contextmanager

from azure.core.credentials import TokenCredential

Expand Down Expand Up @@ -99,6 +100,13 @@ def _get_odata(self) -> _ODataClient:
)
return self._odata

@contextmanager
def _scoped_odata(self) -> Iterator[_ODataClient]:
"""Yield the low-level client while ensuring a correlation scope is active."""
od = self._get_odata()
with od._call_scope():
yield od

# ---------------- Unified CRUD: create/update/delete ----------------
def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[str]:
"""
Expand Down Expand Up @@ -132,19 +140,19 @@ def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dic
ids = client.create("account", records)
print(f"Created {len(ids)} accounts")
"""
od = self._get_odata()
entity_set = od._entity_set_from_schema_name(table_schema_name)
if isinstance(records, dict):
rid = od._create(entity_set, table_schema_name, records)
# _create returns str on single input
if not isinstance(rid, str):
raise TypeError("_create (single) did not return GUID string")
return [rid]
if isinstance(records, list):
ids = od._create_multiple(entity_set, table_schema_name, records)
if not isinstance(ids, list) or not all(isinstance(x, str) for x in ids):
raise TypeError("_create (multi) did not return list[str]")
return ids
with self._scoped_odata() as od:
entity_set = od._entity_set_from_schema_name(table_schema_name)
if isinstance(records, dict):
rid = od._create(entity_set, table_schema_name, records)
# _create returns str on single input
if not isinstance(rid, str):
raise TypeError("_create (single) did not return GUID string")
return [rid]
if isinstance(records, list):
ids = od._create_multiple(entity_set, table_schema_name, records)
if not isinstance(ids, list) or not all(isinstance(x, str) for x in ids):
raise TypeError("_create (multi) did not return list[str]")
return ids
raise TypeError("records must be dict or list[dict]")

def update(
Expand Down Expand Up @@ -192,16 +200,16 @@ def update(
]
client.update("account", ids, changes)
"""
od = self._get_odata()
if isinstance(ids, str):
if not isinstance(changes, dict):
raise TypeError("For single id, changes must be a dict")
od._update(table_schema_name, ids, changes) # discard representation
with self._scoped_odata() as od:
if isinstance(ids, str):
if not isinstance(changes, dict):
raise TypeError("For single id, changes must be a dict")
od._update(table_schema_name, ids, changes) # discard representation
return None
if not isinstance(ids, list):
raise TypeError("ids must be str or list[str]")
od._update_by_ids(table_schema_name, ids, changes)
return None
if not isinstance(ids, list):
raise TypeError("ids must be str or list[str]")
od._update_by_ids(table_schema_name, ids, changes)
return None

def delete(
self,
Expand Down Expand Up @@ -235,21 +243,21 @@ def delete(

job_id = client.delete("account", [id1, id2, id3])
"""
od = self._get_odata()
if isinstance(ids, str):
od._delete(table_schema_name, ids)
return None
if not isinstance(ids, list):
raise TypeError("ids must be str or list[str]")
if not ids:
with self._scoped_odata() as od:
if isinstance(ids, str):
od._delete(table_schema_name, ids)
return None
if not isinstance(ids, list):
raise TypeError("ids must be str or list[str]")
if not ids:
return None
if not all(isinstance(rid, str) for rid in ids):
raise TypeError("ids must contain string GUIDs")
if use_bulk_delete:
return od._delete_multiple(table_schema_name, ids)
for rid in ids:
od._delete(table_schema_name, rid)
return None
if not all(isinstance(rid, str) for rid in ids):
raise TypeError("ids must contain string GUIDs")
if use_bulk_delete:
return od._delete_multiple(table_schema_name, ids)
for rid in ids:
od._delete(table_schema_name, rid)
return None

def get(
self,
Expand Down Expand Up @@ -328,24 +336,29 @@ def get(
):
print(f"Batch size: {len(batch)}")
"""
od = self._get_odata()
if record_id is not None:
if not isinstance(record_id, str):
raise TypeError("record_id must be str")
return od._get(
table_schema_name,
record_id,
select=select,
)
return od._get_multiple(
table_schema_name,
select=select,
filter=filter,
orderby=orderby,
top=top,
expand=expand,
page_size=page_size,
)
with self._scoped_odata() as od:
return od._get(
table_schema_name,
record_id,
select=select,
)

def _paged() -> Iterable[List[Dict[str, Any]]]:
with self._scoped_odata() as od:
yield from od._get_multiple(
table_schema_name,
select=select,
filter=filter,
orderby=orderby,
top=top,
expand=expand,
page_size=page_size,
)

return _paged()

# SQL via Web API sql parameter
def query_sql(self, sql: str):
Expand Down Expand Up @@ -381,7 +394,8 @@ def query_sql(self, sql: str):
sql = "SELECT a.name, a.telephone1 FROM account AS a WHERE a.statecode = 0"
results = client.query_sql(sql)
"""
return self._get_odata()._query_sql(sql)
with self._scoped_odata() as od:
return od._query_sql(sql)

# Table metadata helpers
def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
Expand All @@ -404,7 +418,8 @@ def get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
print(f"Logical name: {info['table_logical_name']}")
print(f"Entity set: {info['entity_set_name']}")
"""
return self._get_odata()._get_table_info(table_schema_name)
with self._scoped_odata() as od:
return od._get_table_info(table_schema_name)

def create_table(
self,
Expand Down Expand Up @@ -474,12 +489,13 @@ class ItemStatus(IntEnum):
primary_column_schema_name="new_ProductName"
)
"""
return self._get_odata()._create_table(
table_schema_name,
columns,
solution_unique_name,
primary_column_schema_name,
)
with self._scoped_odata() as od:
return od._create_table(
table_schema_name,
columns,
solution_unique_name,
primary_column_schema_name,
)

def delete_table(self, table_schema_name: str) -> None:
"""
Expand All @@ -499,7 +515,8 @@ def delete_table(self, table_schema_name: str) -> None:

client.delete_table("new_MyTestTable")
"""
self._get_odata()._delete_table(table_schema_name)
with self._scoped_odata() as od:
od._delete_table(table_schema_name)

def list_tables(self) -> list[str]:
"""
Expand All @@ -515,7 +532,8 @@ def list_tables(self) -> list[str]:
for table in tables:
print(table)
"""
return self._get_odata()._list_tables()
with self._scoped_odata() as od:
return od._list_tables()

def create_columns(
self,
Expand Down Expand Up @@ -545,10 +563,11 @@ def create_columns(
)
print(created) # ['new_Scratch', 'new_Flags']
"""
return self._get_odata()._create_columns(
table_schema_name,
columns,
)
with self._scoped_odata() as od:
return od._create_columns(
table_schema_name,
columns,
)

def delete_columns(
self,
Expand All @@ -573,10 +592,11 @@ def delete_columns(
)
print(removed) # ['new_Scratch', 'new_Flags']
"""
return self._get_odata()._delete_columns(
table_schema_name,
columns,
)
with self._scoped_odata() as od:
return od._delete_columns(
table_schema_name,
columns,
)

# File upload
def upload_file(
Expand Down Expand Up @@ -640,18 +660,18 @@ def upload_file(
mode="auto"
)
"""
od = self._get_odata()
entity_set = od._entity_set_from_schema_name(table_schema_name)
od._upload_file(
entity_set,
record_id,
file_name_attribute,
path,
mode=mode,
mime_type=mime_type,
if_none_match=if_none_match,
)
return None
with self._scoped_odata() as od:
entity_set = od._entity_set_from_schema_name(table_schema_name)
od._upload_file(
entity_set,
record_id,
file_name_attribute,
path,
mode=mode,
mime_type=mime_type,
if_none_match=if_none_match,
)
return None

# Cache utilities
def flush_cache(self, kind) -> int:
Expand All @@ -675,7 +695,8 @@ def flush_cache(self, kind) -> int:
removed = client.flush_cache("picklist")
print(f"Cleared {removed} cached picklist entries")
"""
return self._get_odata()._flush_cache(kind)
with self._scoped_odata() as od:
return od._flush_cache(kind)


__all__ = ["DataverseClient"]
17 changes: 11 additions & 6 deletions src/PowerPlatform/Dataverse/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ class HttpError(DataverseError):
:type subcode: :class:`str` | None
:param service_error_code: Optional Dataverse-specific error code from the API response.
:type service_error_code: :class:`str` | None
:param correlation_id: Optional correlation ID for tracking requests across services.
:param correlation_id: Optional client-generated correlation ID for tracking requests within an SDK call.
:type correlation_id: :class:`str` | None
:param request_id: Optional request ID from the API response headers.
:type request_id: :class:`str` | None
:param client_request_id: Optional client-generated request ID injected into outbound headers.
:type client_request_id: :class:`str` | None
:param service_request_id: Optional ``x-ms-service-request-id`` value returned by Dataverse servers.
:type service_request_id: :class:`str` | None
:param traceparent: Optional W3C trace context for distributed tracing.
:type traceparent: :class:`str` | None
:param body_excerpt: Optional excerpt of the response body for diagnostics.
Expand All @@ -163,7 +165,8 @@ def __init__(
subcode: Optional[str] = None,
service_error_code: Optional[str] = None,
correlation_id: Optional[str] = None,
request_id: Optional[str] = None,
client_request_id: Optional[str] = None,
service_request_id: Optional[str] = None,
traceparent: Optional[str] = None,
body_excerpt: Optional[str] = None,
retry_after: Optional[int] = None,
Expand All @@ -174,8 +177,10 @@ def __init__(
d["service_error_code"] = service_error_code
if correlation_id is not None:
d["correlation_id"] = correlation_id
if request_id is not None:
d["request_id"] = request_id
if client_request_id is not None:
d["client_request_id"] = client_request_id
if service_request_id is not None:
d["service_request_id"] = service_request_id
if traceparent is not None:
d["traceparent"] = traceparent
if body_excerpt is not None:
Expand Down
Loading
Loading