Skip to content

Commit 3a5dfc3

Browse files
author
Saurabh Badenkal
committed
Validate/strip record_id in get(), fix clear_nulls docstring, remove duplicate helper tests
1 parent 7781d3e commit 3a5dfc3

2 files changed

Lines changed: 20 additions & 97 deletions

File tree

src/PowerPlatform/Dataverse/operations/dataframe.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ def get(
110110
df = client.dataframe.get("account", select=["name"], top=100)
111111
"""
112112
if record_id is not None:
113+
if not isinstance(record_id, str) or not record_id.strip():
114+
raise ValueError("record_id must be a non-empty string")
115+
record_id = record_id.strip()
113116
if any(p is not None for p in (filter, orderby, top, expand, page_size)):
114117
raise ValueError(
115118
"Cannot specify query parameters (filter, orderby, top, "
@@ -219,9 +222,11 @@ def update(
219222
:raises ValueError: If ``changes`` is empty, ``id_column`` is not found in the
220223
DataFrame, ``id_column`` contains invalid (non-string, empty, or whitespace-only)
221224
values, or no updatable columns exist besides ``id_column``.
222-
Rows where all change values are NaN/None (after applying ``clear_nulls``)
223-
are silently skipped. If all rows are skipped, the method returns without
224-
making an API call.
225+
When ``clear_nulls`` is ``False`` (default), rows where all change values
226+
are NaN/None produce empty patches and are silently skipped. If all rows
227+
are skipped, the method returns without making an API call. When
228+
``clear_nulls`` is ``True``, NaN/None values become explicit nulls, so
229+
rows are never skipped.
225230
226231
.. tip::
227232
All rows are sent in a single ``UpdateMultiple`` request (or a

tests/unit/test_dataframe_operations.py

Lines changed: 12 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -12,100 +12,6 @@
1212

1313
from PowerPlatform.Dataverse.client import DataverseClient
1414
from PowerPlatform.Dataverse.operations.dataframe import DataFrameOperations
15-
from PowerPlatform.Dataverse.utils._pandas import dataframe_to_records
16-
17-
18-
class TestDataframeToRecordsHelper(unittest.TestCase):
19-
"""Unit tests for the dataframe_to_records() helper in isolation."""
20-
21-
def test_dataframe_to_records_basic(self):
22-
"""Basic DataFrame with string values is converted correctly."""
23-
df = pd.DataFrame([{"name": "Contoso", "city": "Seattle"}])
24-
result = dataframe_to_records(df)
25-
self.assertEqual(result, [{"name": "Contoso", "city": "Seattle"}])
26-
27-
def test_dataframe_to_records_nan_dropped(self):
28-
"""NaN values are omitted from records when na_as_null=False (default)."""
29-
df = pd.DataFrame([{"name": "Contoso", "telephone1": None}])
30-
result = dataframe_to_records(df)
31-
self.assertNotIn("telephone1", result[0])
32-
33-
def test_dataframe_to_records_nan_as_null(self):
34-
"""NaN values become None when na_as_null=True."""
35-
df = pd.DataFrame([{"name": "Contoso", "telephone1": None}])
36-
result = dataframe_to_records(df, na_as_null=True)
37-
self.assertIn("telephone1", result[0])
38-
self.assertIsNone(result[0]["telephone1"])
39-
40-
def test_dataframe_to_records_timestamp_conversion(self):
41-
"""pd.Timestamp values are converted to ISO 8601 strings."""
42-
ts = pd.Timestamp("2024-01-15 10:30:00")
43-
df = pd.DataFrame([{"createdon": ts}])
44-
result = dataframe_to_records(df)
45-
self.assertEqual(result[0]["createdon"], "2024-01-15T10:30:00")
46-
47-
def test_dataframe_to_records_numpy_int(self):
48-
"""np.int64 values are converted to Python int."""
49-
df = pd.DataFrame([{"priority": np.int64(42)}])
50-
result = dataframe_to_records(df)
51-
self.assertIsInstance(result[0]["priority"], int)
52-
self.assertEqual(result[0]["priority"], 42)
53-
54-
def test_dataframe_to_records_numpy_float(self):
55-
"""np.float64 values are converted to Python float."""
56-
df = pd.DataFrame([{"score": np.float64(3.14)}])
57-
result = dataframe_to_records(df)
58-
self.assertIsInstance(result[0]["score"], float)
59-
self.assertAlmostEqual(result[0]["score"], 3.14)
60-
61-
def test_dataframe_to_records_numpy_bool(self):
62-
"""np.bool_ values are converted to Python bool."""
63-
df = pd.DataFrame([{"active": np.bool_(True)}])
64-
result = dataframe_to_records(df)
65-
self.assertIsInstance(result[0]["active"], bool)
66-
self.assertTrue(result[0]["active"])
67-
68-
def test_dataframe_to_records_list_value(self):
69-
"""Cells containing lists pass through without crashing."""
70-
df = pd.DataFrame([{"tags": ["a", "b", "c"]}])
71-
result = dataframe_to_records(df)
72-
self.assertEqual(result[0]["tags"], ["a", "b", "c"])
73-
74-
def test_dataframe_to_records_dict_value(self):
75-
"""Cells containing dicts pass through without crashing."""
76-
df = pd.DataFrame([{"metadata": {"key": "value"}}])
77-
result = dataframe_to_records(df)
78-
self.assertEqual(result[0]["metadata"], {"key": "value"})
79-
80-
def test_dataframe_to_records_empty_dataframe(self):
81-
"""Empty DataFrame returns an empty list."""
82-
df = pd.DataFrame(columns=["name", "telephone1"])
83-
result = dataframe_to_records(df)
84-
self.assertEqual(result, [])
85-
86-
def test_dataframe_to_records_mixed_types(self):
87-
"""DataFrame with mixed types converts all values correctly."""
88-
ts = pd.Timestamp("2024-06-01")
89-
df = pd.DataFrame(
90-
[
91-
{
92-
"name": "Contoso",
93-
"count": np.int64(5),
94-
"score": np.float64(9.8),
95-
"active": np.bool_(True),
96-
"createdon": ts,
97-
"notes": None,
98-
}
99-
]
100-
)
101-
result = dataframe_to_records(df)
102-
rec = result[0]
103-
self.assertEqual(rec["name"], "Contoso")
104-
self.assertIsInstance(rec["count"], int)
105-
self.assertIsInstance(rec["score"], float)
106-
self.assertIsInstance(rec["active"], bool)
107-
self.assertEqual(rec["createdon"], "2024-06-01T00:00:00")
108-
self.assertNotIn("notes", rec)
10915

11016

11117
class TestDataFrameOperationsNamespace(unittest.TestCase):
@@ -187,6 +93,18 @@ def test_get_record_id_with_top_raises(self):
18793
self.client.dataframe.get("account", record_id="guid-1", top=10)
18894
self.assertIn("Cannot specify query parameters", str(ctx.exception))
18995

96+
def test_get_empty_record_id_raises(self):
97+
"""ValueError raised when record_id is empty or whitespace."""
98+
with self.assertRaises(ValueError) as ctx:
99+
self.client.dataframe.get("account", record_id=" ")
100+
self.assertIn("non-empty string", str(ctx.exception))
101+
102+
def test_get_record_id_stripped(self):
103+
"""Leading/trailing whitespace in record_id is stripped."""
104+
self.client._odata._get.return_value = {"accountid": "guid-1", "name": "Contoso"}
105+
self.client.dataframe.get("account", record_id=" guid-1 ")
106+
self.client._odata._get.assert_called_once_with("account", "guid-1", select=None)
107+
190108

191109
class TestDataFrameCreate(unittest.TestCase):
192110
"""Tests for client.dataframe.create()."""

0 commit comments

Comments
 (0)