Skip to content

Commit a0e1163

Browse files
tpellissierclaude
andcommitted
Add optional filter parameter to client.tables.list()
Allow callers to pass an OData $filter expression to narrow the list of returned tables. The user-supplied filter is combined with the existing IsPrivate eq false guard using 'and', so private tables remain excluded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4a828ac commit a0e1163

4 files changed

Lines changed: 116 additions & 5 deletions

File tree

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,16 +1423,29 @@ def _get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
14231423
"columns_created": [],
14241424
}
14251425

1426-
def _list_tables(self) -> List[Dict[str, Any]]:
1426+
def _list_tables(self, filter: Optional[str] = None) -> List[Dict[str, Any]]:
14271427
"""List all non-private tables (``IsPrivate eq false``).
14281428
1429+
:param filter: Optional additional OData ``$filter`` expression that is
1430+
combined with the default ``IsPrivate eq false`` clause using
1431+
``and``. For example, ``"SchemaName eq 'Account'"`` becomes
1432+
``"IsPrivate eq false and SchemaName eq 'Account'"``.
1433+
When ``None`` (the default), only the ``IsPrivate eq false`` filter
1434+
is applied.
1435+
:type filter: ``str`` or ``None``
1436+
14291437
:return: Metadata entries for non-private tables (may be empty).
14301438
:rtype: ``list[dict[str, Any]]``
14311439
14321440
:raises HttpError: If the metadata request fails.
14331441
"""
14341442
url = f"{self.api}/EntityDefinitions"
1435-
params = {"$filter": "IsPrivate eq false"}
1443+
base_filter = "IsPrivate eq false"
1444+
if filter:
1445+
combined_filter = f"{base_filter} and {filter}"
1446+
else:
1447+
combined_filter = base_filter
1448+
params = {"$filter": combined_filter}
14361449
r = self._request("get", url, params=params)
14371450
return r.json().get("value", [])
14381451

src/PowerPlatform/Dataverse/operations/tables.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,20 +179,38 @@ def get(self, table: str) -> Optional[Dict[str, Any]]:
179179

180180
# ------------------------------------------------------------------- list
181181

182-
def list(self) -> List[Dict[str, Any]]:
182+
def list(self, *, filter: Optional[str] = None) -> List[Dict[str, Any]]:
183183
"""List all non-private tables in the Dataverse environment.
184184
185+
By default returns every table where ``IsPrivate eq false``. Supply
186+
an optional OData ``$filter`` expression to further narrow the results.
187+
The expression is combined with the default ``IsPrivate eq false``
188+
clause using ``and``.
189+
190+
:param filter: Optional OData ``$filter`` expression to further narrow
191+
the list of returned tables (e.g.
192+
``"SchemaName eq 'Account'"``). Column names in filter
193+
expressions must use the exact property names from the
194+
``EntityDefinitions`` metadata (typically PascalCase).
195+
:type filter: :class:`str` or None
196+
185197
:return: List of EntityDefinition metadata dictionaries.
186198
:rtype: :class:`list` of :class:`dict`
187199
188200
Example::
189201
202+
# List all non-private tables
190203
tables = client.tables.list()
191204
for table in tables:
192205
print(table["LogicalName"])
206+
207+
# List only tables whose schema name starts with "new_"
208+
custom_tables = client.tables.list(
209+
filter="startswith(SchemaName, 'new_')"
210+
)
193211
"""
194212
with self._client._scoped_odata() as od:
195-
return od._list_tables()
213+
return od._list_tables(filter=filter)
196214

197215
# ------------------------------------------------------------- add_columns
198216

tests/unit/data/test_odata_internal.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,61 @@ def test_non_string_key_raises_type_error(self):
125125
self.od._build_alternate_key_str({1: "ACC-001"})
126126

127127

128+
class TestListTables(unittest.TestCase):
129+
"""Unit tests for _ODataClient._list_tables filter parameter."""
130+
131+
def setUp(self):
132+
self.od = _make_odata_client()
133+
134+
def _setup_response(self, value):
135+
"""Configure _request to return a response with the given value list."""
136+
mock_response = MagicMock()
137+
mock_response.json.return_value = {"value": value}
138+
self.od._request.return_value = mock_response
139+
140+
def test_no_filter_uses_default(self):
141+
"""_list_tables() without filter sends only IsPrivate eq false."""
142+
self._setup_response([])
143+
self.od._list_tables()
144+
145+
self.od._request.assert_called_once()
146+
call_kwargs = self.od._request.call_args
147+
params = call_kwargs.kwargs.get("params") or call_kwargs[1].get("params", {})
148+
self.assertEqual(params["$filter"], "IsPrivate eq false")
149+
150+
def test_filter_combined_with_default(self):
151+
"""_list_tables(filter=...) combines user filter with IsPrivate eq false."""
152+
self._setup_response([{"LogicalName": "account"}])
153+
self.od._list_tables(filter="SchemaName eq 'Account'")
154+
155+
self.od._request.assert_called_once()
156+
call_kwargs = self.od._request.call_args
157+
params = call_kwargs.kwargs.get("params") or call_kwargs[1].get("params", {})
158+
self.assertEqual(
159+
params["$filter"],
160+
"IsPrivate eq false and SchemaName eq 'Account'",
161+
)
162+
163+
def test_filter_none_same_as_no_filter(self):
164+
"""_list_tables(filter=None) is equivalent to _list_tables()."""
165+
self._setup_response([])
166+
self.od._list_tables(filter=None)
167+
168+
call_kwargs = self.od._request.call_args
169+
params = call_kwargs.kwargs.get("params") or call_kwargs[1].get("params", {})
170+
self.assertEqual(params["$filter"], "IsPrivate eq false")
171+
172+
def test_returns_value_list(self):
173+
"""_list_tables returns the 'value' array from the response."""
174+
expected = [
175+
{"LogicalName": "account"},
176+
{"LogicalName": "contact"},
177+
]
178+
self._setup_response(expected)
179+
result = self.od._list_tables()
180+
self.assertEqual(result, expected)
181+
182+
128183
class TestUpsert(unittest.TestCase):
129184
"""Unit tests for _ODataClient._upsert."""
130185

tests/unit/test_tables_operations.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,35 @@ def test_list(self):
100100

101101
result = self.client.tables.list()
102102

103-
self.client._odata._list_tables.assert_called_once()
103+
self.client._odata._list_tables.assert_called_once_with(filter=None)
104104
self.assertIsInstance(result, list)
105105
self.assertEqual(result, expected_tables)
106106

107+
def test_list_with_filter(self):
108+
"""list(filter=...) should pass the filter expression to _list_tables."""
109+
expected_tables = [
110+
{"LogicalName": "account", "SchemaName": "Account"},
111+
]
112+
self.client._odata._list_tables.return_value = expected_tables
113+
114+
result = self.client.tables.list(filter="SchemaName eq 'Account'")
115+
116+
self.client._odata._list_tables.assert_called_once_with(filter="SchemaName eq 'Account'")
117+
self.assertIsInstance(result, list)
118+
self.assertEqual(result, expected_tables)
119+
120+
def test_list_with_filter_none_explicit(self):
121+
"""list(filter=None) should behave identically to list() with no args."""
122+
expected_tables = [
123+
{"LogicalName": "account", "SchemaName": "Account"},
124+
]
125+
self.client._odata._list_tables.return_value = expected_tables
126+
127+
result = self.client.tables.list(filter=None)
128+
129+
self.client._odata._list_tables.assert_called_once_with(filter=None)
130+
self.assertEqual(result, expected_tables)
131+
107132
# ------------------------------------------------------------ add_columns
108133

109134
def test_add_columns(self):

0 commit comments

Comments
 (0)