Skip to content

Commit 0123728

Browse files
tpellissierclaude
andcommitted
Integrate FluentResult into client.py for telemetry access (Phase 1.3)
Updates DataverseClient methods to return FluentResult wrappers that: - Act like the original return types by default (backward compatible) - Support .with_detail_response() for accessing telemetry Changes to client.py: - Import FluentResult and RequestMetadata from core.results - create() now returns FluentResult[List[str]] with batch_info - update() now returns FluentResult[None] with metadata - delete() now returns FluentResult[Optional[str]] with metadata - get() for single record returns FluentResult[Dict] with metadata - get() for multiple records unchanged (returns iterator) Backward compatibility maintained: - ids = client.create("account", {"name": "A"}) # Works as before - ids[0] # Works via __getitem__ - for id in ids: # Works via __iter__ - len(ids) # Works via __len__ New telemetry access: - response = client.create(...).with_detail_response() - response.telemetry['timing_ms'] # Request duration - response.telemetry['client_request_id'] # For tracing - response.telemetry['batch_info'] # For bulk operations Updates test_client.py: - Tests now use _with_metadata mock methods - Added tests for .with_detail_response() pattern - All 10 client tests pass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0e38180 commit 0123728

2 files changed

Lines changed: 172 additions & 56 deletions

File tree

src/PowerPlatform/Dataverse/client.py

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .core._auth import _AuthManager
1212
from .core.config import DataverseConfig
13+
from .core.results import FluentResult, RequestMetadata
1314
from .data._odata import _ODataClient
1415

1516

@@ -108,7 +109,9 @@ def _scoped_odata(self) -> Iterator[_ODataClient]:
108109
yield od
109110

110111
# ---------------- Unified CRUD: create/update/delete ----------------
111-
def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[str]:
112+
def create(
113+
self, table_schema_name: str, records: Union[Dict[str, Any], List[Dict[str, Any]]]
114+
) -> FluentResult[List[str]]:
112115
"""
113116
Create one or more records by table name.
114117
@@ -118,8 +121,9 @@ def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dic
118121
Each dictionary should contain column schema names as keys.
119122
:type records: :class:`dict` or :class:`list` of :class:`dict`
120123
121-
:return: List of created record GUIDs. Returns a single-element list for a single input.
122-
:rtype: :class:`list` of :class:`str`
124+
:return: FluentResult wrapping list of created record GUIDs. Acts like a list
125+
by default. Call ``.with_detail_response()`` for telemetry.
126+
:rtype: :class:`~PowerPlatform.Dataverse.core.results.FluentResult` of :class:`list` of :class:`str`
123127
124128
:raises TypeError: If ``records`` is not a dict or list[dict], or if the internal
125129
client returns an unexpected type.
@@ -139,25 +143,31 @@ def create(self, table_schema_name: str, records: Union[Dict[str, Any], List[Dic
139143
]
140144
ids = client.create("account", records)
141145
print(f"Created {len(ids)} accounts")
146+
147+
Access telemetry with ``.with_detail_response()``::
148+
149+
response = client.create("account", {"name": "Test"}).with_detail_response()
150+
print(f"Timing: {response.telemetry['timing_ms']}ms")
142151
"""
143152
with self._scoped_odata() as od:
144153
entity_set = od._entity_set_from_schema_name(table_schema_name)
145154
if isinstance(records, dict):
146-
rid = od._create(entity_set, table_schema_name, records)
147-
# _create returns str on single input
155+
rid, metadata = od._create_with_metadata(entity_set, table_schema_name, records)
148156
if not isinstance(rid, str):
149157
raise TypeError("_create (single) did not return GUID string")
150-
return [rid]
158+
return FluentResult([rid], metadata, batch_info={"total": 1, "success": 1, "failures": 0})
151159
if isinstance(records, list):
152-
ids = od._create_multiple(entity_set, table_schema_name, records)
160+
ids, metadata, batch_info = od._create_multiple_with_metadata(
161+
entity_set, table_schema_name, records
162+
)
153163
if not isinstance(ids, list) or not all(isinstance(x, str) for x in ids):
154164
raise TypeError("_create (multi) did not return list[str]")
155-
return ids
165+
return FluentResult(ids, metadata, batch_info=batch_info)
156166
raise TypeError("records must be dict or list[dict]")
157167

158168
def update(
159169
self, table_schema_name: str, ids: Union[str, List[str]], changes: Union[Dict[str, Any], List[Dict[str, Any]]]
160-
) -> None:
170+
) -> FluentResult[None]:
161171
"""
162172
Update one or more records.
163173
@@ -177,6 +187,9 @@ def update(
177187
have equal length for one-to-one mapping.
178188
:type changes: :class:`dict` or :class:`list` of :class:`dict`
179189
190+
:return: FluentResult wrapping None. Call ``.with_detail_response()`` for telemetry.
191+
:rtype: :class:`~PowerPlatform.Dataverse.core.results.FluentResult` of ``None``
192+
180193
:raises TypeError: If ``ids`` is not str or list[str], or if ``changes`` type doesn't match usage pattern.
181194
182195
.. note::
@@ -199,24 +212,34 @@ def update(
199212
{"name": "Updated Name 2"}
200213
]
201214
client.update("account", ids, changes)
215+
216+
Access telemetry with ``.with_detail_response()``::
217+
218+
response = client.update("account", id, {"name": "Test"}).with_detail_response()
219+
print(f"Timing: {response.telemetry['timing_ms']}ms")
202220
"""
203221
with self._scoped_odata() as od:
204222
if isinstance(ids, str):
205223
if not isinstance(changes, dict):
206224
raise TypeError("For single id, changes must be a dict")
207-
od._update(table_schema_name, ids, changes) # discard representation
208-
return None
225+
_, metadata = od._update_with_metadata(table_schema_name, ids, changes)
226+
return FluentResult(None, metadata)
209227
if not isinstance(ids, list):
210228
raise TypeError("ids must be str or list[str]")
229+
# For bulk updates, we still use the original method as _update_by_ids doesn't have a _with_metadata variant yet
230+
# TODO: Add _update_by_ids_with_metadata for bulk update telemetry
211231
od._update_by_ids(table_schema_name, ids, changes)
212-
return None
232+
# Create placeholder metadata for bulk updates
233+
placeholder_metadata = RequestMetadata()
234+
num_updates = len(ids)
235+
return FluentResult(None, placeholder_metadata, batch_info={"total": num_updates, "success": num_updates, "failures": 0})
213236

214237
def delete(
215238
self,
216239
table_schema_name: str,
217240
ids: Union[str, List[str]],
218241
use_bulk_delete: bool = True,
219-
) -> Optional[str]:
242+
) -> FluentResult[Optional[str]]:
220243
"""
221244
Delete one or more records by GUID.
222245
@@ -228,12 +251,13 @@ def delete(
228251
return its async job identifier. When ``False`` each record is deleted sequentially.
229252
:type use_bulk_delete: :class:`bool`
230253
254+
:return: FluentResult wrapping BulkDelete job ID (for bulk) or None (for single).
255+
Call ``.with_detail_response()`` for telemetry.
256+
:rtype: :class:`~PowerPlatform.Dataverse.core.results.FluentResult` of :class:`str` or ``None``
257+
231258
:raises TypeError: If ``ids`` is not str or list[str].
232259
:raises HttpError: If the underlying Web API delete request fails.
233260
234-
:return: BulkDelete job ID when deleting multiple records via BulkDelete; otherwise ``None``.
235-
:rtype: :class:`str` or None
236-
237261
Example:
238262
Delete a single record::
239263
@@ -242,22 +266,31 @@ def delete(
242266
Delete multiple records::
243267
244268
job_id = client.delete("account", [id1, id2, id3])
269+
270+
Access telemetry with ``.with_detail_response()``::
271+
272+
response = client.delete("account", account_id).with_detail_response()
273+
print(f"Timing: {response.telemetry['timing_ms']}ms")
245274
"""
246275
with self._scoped_odata() as od:
247276
if isinstance(ids, str):
248-
od._delete(table_schema_name, ids)
249-
return None
277+
_, metadata = od._delete_with_metadata(table_schema_name, ids)
278+
return FluentResult(None, metadata)
250279
if not isinstance(ids, list):
251280
raise TypeError("ids must be str or list[str]")
252281
if not ids:
253-
return None
282+
return FluentResult(None, RequestMetadata())
254283
if not all(isinstance(rid, str) for rid in ids):
255284
raise TypeError("ids must contain string GUIDs")
256285
if use_bulk_delete:
257-
return od._delete_multiple(table_schema_name, ids)
286+
# TODO: Add _delete_multiple_with_metadata for bulk delete telemetry
287+
job_id = od._delete_multiple(table_schema_name, ids)
288+
return FluentResult(job_id, RequestMetadata(), batch_info={"total": len(ids)})
289+
# Sequential deletes
290+
last_metadata = RequestMetadata()
258291
for rid in ids:
259-
od._delete(table_schema_name, rid)
260-
return None
292+
_, last_metadata = od._delete_with_metadata(table_schema_name, rid)
293+
return FluentResult(None, last_metadata, batch_info={"total": len(ids), "success": len(ids), "failures": 0})
261294

262295
def get(
263296
self,
@@ -269,11 +302,11 @@ def get(
269302
top: Optional[int] = None,
270303
expand: Optional[List[str]] = None,
271304
page_size: Optional[int] = None,
272-
) -> Union[Dict[str, Any], Iterable[List[Dict[str, Any]]]]:
305+
) -> Union[FluentResult[Dict[str, Any]], Iterable[List[Dict[str, Any]]]]:
273306
"""
274307
Fetch a single record by ID or query multiple records.
275308
276-
When ``record_id`` is provided, returns a single record dictionary.
309+
When ``record_id`` is provided, returns a FluentResult wrapping the record dictionary.
277310
When ``record_id`` is None, returns a generator yielding batches of records.
278311
279312
:param table_schema_name: Schema name of the table (e.g. ``"account"`` or ``"new_MyTestTable"``).
@@ -293,9 +326,11 @@ def get(
293326
:param page_size: Optional number of records per page for pagination.
294327
:type page_size: :class:`int` or None
295328
296-
:return: Single record dict if ``record_id`` is provided, otherwise a generator
329+
:return: FluentResult wrapping single record dict if ``record_id`` is provided
330+
(call ``.with_detail_response()`` for telemetry), otherwise a generator
297331
yielding lists of record dictionaries (one list per page).
298-
:rtype: :class:`dict` or :class:`collections.abc.Iterable` of :class:`list` of :class:`dict`
332+
:rtype: :class:`~PowerPlatform.Dataverse.core.results.FluentResult` of :class:`dict`
333+
or :class:`collections.abc.Iterable` of :class:`list` of :class:`dict`
299334
300335
:raises TypeError: If ``record_id`` is provided but not a string.
301336
@@ -305,6 +340,11 @@ def get(
305340
record = client.get("account", record_id=account_id, select=["name", "telephone1"])
306341
print(record["name"])
307342
343+
Access telemetry for single record fetch::
344+
345+
response = client.get("account", record_id=account_id).with_detail_response()
346+
print(f"Timing: {response.telemetry['timing_ms']}ms")
347+
308348
Query multiple records with filtering (note: exact logical names in filter)::
309349
310350
for batch in client.get(
@@ -340,11 +380,12 @@ def get(
340380
if not isinstance(record_id, str):
341381
raise TypeError("record_id must be str")
342382
with self._scoped_odata() as od:
343-
return od._get(
383+
record, metadata = od._get_with_metadata(
344384
table_schema_name,
345385
record_id,
346386
select=select,
347387
)
388+
return FluentResult(record, metadata)
348389

349390
def _paged() -> Iterable[List[Dict[str, Any]]]:
350391
with self._scoped_odata() as od:

0 commit comments

Comments
 (0)