Skip to content

Commit e9a25aa

Browse files
Copilotsaurabhrb
andcommitted
Add list_columns, list_relationships, list_table_relationships APIs with tests and README examples
Co-authored-by: saurabhrb <32964911+saurabhrb@users.noreply.github.com>
1 parent 96d1f28 commit e9a25aa

File tree

7 files changed

+723
-1
lines changed

7 files changed

+723
-1
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,18 @@ client.tables.add_columns("new_Product", {"new_Category": "string"})
307307
# Remove columns
308308
client.tables.remove_columns("new_Product", ["new_Category"])
309309

310+
# List all columns (attributes) for a table to discover schema
311+
columns = client.tables.list_columns("account")
312+
for col in columns:
313+
print(f"{col['LogicalName']} ({col.get('AttributeType')})")
314+
315+
# List only specific properties
316+
columns = client.tables.list_columns(
317+
"account",
318+
select=["LogicalName", "SchemaName", "AttributeType"],
319+
filter="AttributeType eq 'String'",
320+
)
321+
310322
# Clean up
311323
client.tables.delete("new_Product")
312324
```
@@ -359,6 +371,16 @@ rel = client.tables.get_relationship("new_Department_Employee")
359371
if rel:
360372
print(f"Found: {rel['SchemaName']}")
361373

374+
# List all relationships
375+
rels = client.tables.list_relationships()
376+
for rel in rels:
377+
print(f"{rel['SchemaName']} ({rel.get('@odata.type')})")
378+
379+
# List relationships for a specific table (one-to-many + many-to-many)
380+
account_rels = client.tables.list_table_relationships("account")
381+
for rel in account_rels:
382+
print(f"{rel['SchemaName']} -> {rel.get('@odata.type')}")
383+
362384
# Delete a relationship
363385
client.tables.delete_relationship(result['relationship_id'])
364386
```

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,50 @@ def _get_attribute_metadata(
10391039
return item
10401040
return None
10411041

1042+
def _list_columns(
1043+
self,
1044+
table_schema_name: str,
1045+
*,
1046+
select: Optional[List[str]] = None,
1047+
filter: Optional[str] = None,
1048+
) -> List[Dict[str, Any]]:
1049+
"""List all attribute (column) definitions for a table.
1050+
1051+
Issues ``GET EntityDefinitions({MetadataId})/Attributes`` with optional
1052+
``$select`` and ``$filter`` query parameters.
1053+
1054+
:param table_schema_name: Schema name of the table
1055+
(e.g. ``"account"`` or ``"new_Product"``).
1056+
:type table_schema_name: ``str``
1057+
:param select: Optional list of property names to project via
1058+
``$select``. Values are passed as-is (PascalCase).
1059+
:type select: ``list[str]`` or ``None``
1060+
:param filter: Optional OData ``$filter`` expression. For example,
1061+
``"AttributeType eq 'String'"`` returns only string columns.
1062+
:type filter: ``str`` or ``None``
1063+
1064+
:return: List of raw attribute metadata dictionaries (may be empty).
1065+
:rtype: ``list[dict[str, Any]]``
1066+
1067+
:raises MetadataError: If the table is not found.
1068+
:raises HttpError: If the Web API request fails.
1069+
"""
1070+
ent = self._get_entity_by_table_schema_name(table_schema_name)
1071+
if not ent or not ent.get("MetadataId"):
1072+
raise MetadataError(
1073+
f"Table '{table_schema_name}' not found.",
1074+
subcode=METADATA_TABLE_NOT_FOUND,
1075+
)
1076+
metadata_id = ent["MetadataId"]
1077+
url = f"{self.api}/EntityDefinitions({metadata_id})/Attributes"
1078+
params: Dict[str, str] = {}
1079+
if select:
1080+
params["$select"] = ",".join(select)
1081+
if filter:
1082+
params["$filter"] = filter
1083+
r = self._request("get", url, params=params)
1084+
return r.json().get("value", [])
1085+
10421086
def _wait_for_attribute_visibility(
10431087
self,
10441088
entity_set: str,

src/PowerPlatform/Dataverse/data/_relationships.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from __future__ import annotations
1111

1212
import re
13-
from typing import Any, Dict, Optional
13+
from typing import Any, Dict, List, Optional
1414

1515

1616
class _RelationshipOperationsMixin:
@@ -142,6 +142,93 @@ def _get_relationship(self, schema_name: str) -> Optional[Dict[str, Any]]:
142142
results = data.get("value", [])
143143
return results[0] if results else None
144144

145+
def _list_relationships(
146+
self,
147+
*,
148+
filter: Optional[str] = None,
149+
select: Optional[List[str]] = None,
150+
) -> List[Dict[str, Any]]:
151+
"""List all relationship definitions.
152+
153+
Issues ``GET /RelationshipDefinitions`` with optional ``$filter`` and
154+
``$select`` query parameters.
155+
156+
:param filter: Optional OData ``$filter`` expression. For example,
157+
``"RelationshipType eq Microsoft.Dynamics.CRM.RelationshipType'OneToManyRelationship'"``
158+
returns only one-to-many relationships.
159+
:type filter: ``str`` or ``None``
160+
:param select: Optional list of property names to project via
161+
``$select``. Values are passed as-is (PascalCase).
162+
:type select: ``list[str]`` or ``None``
163+
164+
:return: List of raw relationship metadata dictionaries (may be empty).
165+
:rtype: ``list[dict[str, Any]]``
166+
167+
:raises HttpError: If the Web API request fails.
168+
"""
169+
url = f"{self.api}/RelationshipDefinitions"
170+
params: Dict[str, str] = {}
171+
if filter:
172+
params["$filter"] = filter
173+
if select:
174+
params["$select"] = ",".join(select)
175+
r = self._request("get", url, headers=self._headers(), params=params)
176+
return r.json().get("value", [])
177+
178+
def _list_table_relationships(
179+
self,
180+
table_schema_name: str,
181+
*,
182+
filter: Optional[str] = None,
183+
select: Optional[List[str]] = None,
184+
) -> List[Dict[str, Any]]:
185+
"""List all relationships for a specific table.
186+
187+
Issues ``GET EntityDefinitions({MetadataId})/OneToManyRelationships``
188+
and ``GET EntityDefinitions({MetadataId})/ManyToManyRelationships``,
189+
then combines the results.
190+
191+
:param table_schema_name: Schema name of the table (e.g. ``"account"``).
192+
:type table_schema_name: ``str``
193+
:param filter: Optional OData ``$filter`` expression applied to each
194+
sub-request.
195+
:type filter: ``str`` or ``None``
196+
:param select: Optional list of property names to project via
197+
``$select``. Values are passed as-is (PascalCase).
198+
:type select: ``list[str]`` or ``None``
199+
200+
:return: Combined list of one-to-many and many-to-many relationship
201+
metadata dictionaries (may be empty).
202+
:rtype: ``list[dict[str, Any]]``
203+
204+
:raises MetadataError: If the table is not found.
205+
:raises HttpError: If the Web API request fails.
206+
"""
207+
from ..core.errors import MetadataError
208+
from ..core._error_codes import METADATA_TABLE_NOT_FOUND
209+
210+
ent = self._get_entity_by_table_schema_name(table_schema_name)
211+
if not ent or not ent.get("MetadataId"):
212+
raise MetadataError(
213+
f"Table '{table_schema_name}' not found.",
214+
subcode=METADATA_TABLE_NOT_FOUND,
215+
)
216+
217+
metadata_id = ent["MetadataId"]
218+
params: Dict[str, str] = {}
219+
if filter:
220+
params["$filter"] = filter
221+
if select:
222+
params["$select"] = ",".join(select)
223+
224+
one_to_many_url = f"{self.api}/EntityDefinitions({metadata_id})/OneToManyRelationships"
225+
many_to_many_url = f"{self.api}/EntityDefinitions({metadata_id})/ManyToManyRelationships"
226+
227+
r1 = self._request("get", one_to_many_url, headers=self._headers(), params=params)
228+
r2 = self._request("get", many_to_many_url, headers=self._headers(), params=params)
229+
230+
return r1.json().get("value", []) + r2.json().get("value", [])
231+
145232
def _extract_id_from_header(self, header_value: Optional[str]) -> Optional[str]:
146233
"""
147234
Extract a GUID from an OData-EntityId header value.

src/PowerPlatform/Dataverse/operations/tables.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,3 +706,142 @@ def delete_alternate_key(self, table: str, key_id: str) -> None:
706706
"""
707707
with self._client._scoped_odata() as od:
708708
od._delete_alternate_key(table, key_id)
709+
710+
# -------------------------------------------------------- list_columns
711+
712+
def list_columns(
713+
self,
714+
table: str,
715+
*,
716+
select: Optional[List[str]] = None,
717+
filter: Optional[str] = None,
718+
) -> List[Dict[str, Any]]:
719+
"""List all attribute (column) definitions for a table.
720+
721+
:param table: Schema name of the table (e.g. ``"account"`` or
722+
``"new_Product"``).
723+
:type table: :class:`str`
724+
:param select: Optional list of property names to project via
725+
``$select``. Values are passed as-is (PascalCase).
726+
:type select: :class:`list` of :class:`str` or None
727+
:param filter: Optional OData ``$filter`` expression. For example,
728+
``"AttributeType eq 'String'"`` returns only string columns.
729+
:type filter: :class:`str` or None
730+
731+
:return: List of raw attribute metadata dictionaries.
732+
:rtype: :class:`list` of :class:`dict`
733+
734+
:raises ~PowerPlatform.Dataverse.core.errors.MetadataError:
735+
If the table is not found.
736+
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
737+
If the Web API request fails.
738+
739+
Example::
740+
741+
# List all columns on the account table
742+
columns = client.tables.list_columns("account")
743+
for col in columns:
744+
print(f"{col['LogicalName']} ({col.get('AttributeType')})")
745+
746+
# List only specific properties
747+
columns = client.tables.list_columns(
748+
"account",
749+
select=["LogicalName", "SchemaName", "AttributeType"],
750+
)
751+
752+
# Filter to only string attributes
753+
columns = client.tables.list_columns(
754+
"account",
755+
filter="AttributeType eq 'String'",
756+
)
757+
"""
758+
with self._client._scoped_odata() as od:
759+
return od._list_columns(table, select=select, filter=filter)
760+
761+
# ------------------------------------------------- list_relationships
762+
763+
def list_relationships(
764+
self,
765+
*,
766+
filter: Optional[str] = None,
767+
select: Optional[List[str]] = None,
768+
) -> List[Dict[str, Any]]:
769+
"""List all relationship definitions in the environment.
770+
771+
:param filter: Optional OData ``$filter`` expression. For example,
772+
``"RelationshipType eq Microsoft.Dynamics.CRM.RelationshipType'OneToManyRelationship'"``
773+
returns only one-to-many relationships.
774+
:type filter: :class:`str` or None
775+
:param select: Optional list of property names to project via
776+
``$select``. Values are passed as-is (PascalCase).
777+
:type select: :class:`list` of :class:`str` or None
778+
779+
:return: List of raw relationship metadata dictionaries.
780+
:rtype: :class:`list` of :class:`dict`
781+
782+
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
783+
If the Web API request fails.
784+
785+
Example::
786+
787+
# List all relationships
788+
rels = client.tables.list_relationships()
789+
for rel in rels:
790+
print(f"{rel['SchemaName']} ({rel.get('@odata.type')})")
791+
792+
# Filter by type
793+
one_to_many = client.tables.list_relationships(
794+
filter="RelationshipType eq Microsoft.Dynamics.CRM.RelationshipType'OneToManyRelationship'"
795+
)
796+
797+
# Select specific properties
798+
rels = client.tables.list_relationships(
799+
select=["SchemaName", "ReferencedEntity", "ReferencingEntity"]
800+
)
801+
"""
802+
with self._client._scoped_odata() as od:
803+
return od._list_relationships(filter=filter, select=select)
804+
805+
# --------------------------------------------- list_table_relationships
806+
807+
def list_table_relationships(
808+
self,
809+
table: str,
810+
*,
811+
filter: Optional[str] = None,
812+
select: Optional[List[str]] = None,
813+
) -> List[Dict[str, Any]]:
814+
"""List all relationships for a specific table.
815+
816+
Combines one-to-many and many-to-many relationships for the given
817+
table by querying both
818+
``EntityDefinitions({id})/OneToManyRelationships`` and
819+
``EntityDefinitions({id})/ManyToManyRelationships``.
820+
821+
:param table: Schema name of the table (e.g. ``"account"``).
822+
:type table: :class:`str`
823+
:param filter: Optional OData ``$filter`` expression applied to each
824+
sub-request.
825+
:type filter: :class:`str` or None
826+
:param select: Optional list of property names to project via
827+
``$select``. Values are passed as-is (PascalCase).
828+
:type select: :class:`list` of :class:`str` or None
829+
830+
:return: Combined list of one-to-many and many-to-many relationship
831+
metadata dictionaries.
832+
:rtype: :class:`list` of :class:`dict`
833+
834+
:raises ~PowerPlatform.Dataverse.core.errors.MetadataError:
835+
If the table is not found.
836+
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
837+
If the Web API request fails.
838+
839+
Example::
840+
841+
# List all relationships for the account table
842+
rels = client.tables.list_table_relationships("account")
843+
for rel in rels:
844+
print(f"{rel['SchemaName']} -> {rel.get('@odata.type')}")
845+
"""
846+
with self._client._scoped_odata() as od:
847+
return od._list_table_relationships(table, filter=filter, select=select)

0 commit comments

Comments
 (0)