Skip to content

Commit 9140589

Browse files
committed
Refactor _ODataClient to improve $select parameter handling and enhance TableInfo model with deprecated property aliases and additional relationship attributes
1 parent a4356ed commit 9140589

File tree

3 files changed

+86
-34
lines changed

3 files changed

+86
-34
lines changed

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,9 +1535,14 @@ def _get_table_metadata(
15351535
params: Dict[str, str] = {}
15361536
if select is not None and isinstance(select, str):
15371537
raise TypeError("select must be a list of property names, not a bare string")
1538-
base_fields = {"MetadataId", "LogicalName", "SchemaName", "EntitySetName"}
1538+
base_fields = ["EntitySetName", "LogicalName", "MetadataId", "SchemaName"]
15391539
if select:
1540-
merged = list(base_fields | set(select))
1540+
seen = set(base_fields)
1541+
merged = list(base_fields)
1542+
for f in select:
1543+
if f not in seen:
1544+
merged.append(f)
1545+
seen.add(f)
15411546
else:
15421547
merged = list(base_fields)
15431548
params["$select"] = ",".join(merged)

src/PowerPlatform/Dataverse/models/table_info.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import warnings
89
from dataclasses import dataclass, field
910
from typing import Any, ClassVar, Dict, Iterator, KeysView, List, Optional
1011

@@ -63,6 +64,38 @@ class ColumnInfo:
6364
max_length: Optional[int] = None
6465
metadata_id: Optional[str] = None
6566

67+
# ---------------------------------------------- deprecated property aliases
68+
69+
@property
70+
def type(self) -> str:
71+
"""Column type name (deprecated, use ``attribute_type_name`` or ``attribute_type``)."""
72+
warnings.warn(
73+
"ColumnInfo.type is deprecated. Use attribute_type_name or attribute_type instead.",
74+
DeprecationWarning,
75+
stacklevel=2,
76+
)
77+
return self.attribute_type_name or self.attribute_type
78+
79+
@property
80+
def is_primary(self) -> bool:
81+
"""Whether this is the primary name column (deprecated, use ``is_primary_name``)."""
82+
warnings.warn(
83+
"ColumnInfo.is_primary is deprecated. Use is_primary_name instead.",
84+
DeprecationWarning,
85+
stacklevel=2,
86+
)
87+
return self.is_primary_name
88+
89+
@property
90+
def is_required(self) -> bool:
91+
"""Whether the column is required (deprecated, use ``required_level``)."""
92+
warnings.warn(
93+
"ColumnInfo.is_required is deprecated. Use required_level instead.",
94+
DeprecationWarning,
95+
stacklevel=2,
96+
)
97+
return self.required_level not in (None, "", "None")
98+
6699
@classmethod
67100
def from_api_response(cls, data: Dict[str, Any]) -> ColumnInfo:
68101
"""Create from a raw Dataverse ``AttributeMetadata`` API response.
@@ -154,6 +187,10 @@ class TableInfo:
154187
description: Optional[str] = None
155188
columns: Optional[List[ColumnInfo]] = field(default=None, repr=False)
156189
columns_created: Optional[List[str]] = field(default=None, repr=False)
190+
one_to_many_relationships: Optional[List[Dict[str, Any]]] = field(default=None, repr=False)
191+
many_to_one_relationships: Optional[List[Dict[str, Any]]] = field(default=None, repr=False)
192+
many_to_many_relationships: Optional[List[Dict[str, Any]]] = field(default=None, repr=False)
193+
_extra: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
157194

158195
# Maps legacy dict keys (used by existing code) to attribute names.
159196
_LEGACY_KEY_MAP: ClassVar[Dict[str, str]] = {
@@ -171,16 +208,24 @@ def _resolve_key(self, key: str) -> str:
171208
return self._LEGACY_KEY_MAP.get(key, key)
172209

173210
def __getitem__(self, key: str) -> Any:
211+
if key in self._extra:
212+
return self._extra[key]
174213
attr = self._resolve_key(key)
175214
if hasattr(self, attr):
176-
return getattr(self, attr)
215+
val = getattr(self, attr)
216+
if val is not None or key in self._LEGACY_KEY_MAP:
217+
return val
177218
raise KeyError(key)
178219

179220
def __contains__(self, key: object) -> bool:
180221
if not isinstance(key, str):
181222
return False
223+
if key in self._extra:
224+
return True
182225
attr = self._resolve_key(key)
183-
return hasattr(self, attr)
226+
if not hasattr(self, attr):
227+
return False
228+
return getattr(self, attr) is not None
184229

185230
def __iter__(self) -> Iterator[str]:
186231
return iter(self._LEGACY_KEY_MAP)
@@ -246,13 +291,22 @@ def from_api_response(cls, response_data: Dict[str, Any]) -> TableInfo:
246291
desc_label = desc_obj.get("UserLocalizedLabel") or {}
247292
description = desc_label.get("Label")
248293

294+
# Parse columns if Attributes are present
295+
columns = None
296+
if "Attributes" in response_data:
297+
columns = [ColumnInfo.from_api_response(a) for a in response_data["Attributes"]]
298+
249299
return cls(
250300
schema_name=response_data.get("SchemaName", ""),
251301
logical_name=response_data.get("LogicalName", ""),
252302
entity_set_name=response_data.get("EntitySetName", ""),
253303
metadata_id=response_data.get("MetadataId", ""),
254304
display_name=display_name,
255305
description=description,
306+
columns=columns,
307+
one_to_many_relationships=response_data.get("OneToManyRelationships"),
308+
many_to_one_relationships=response_data.get("ManyToOneRelationships"),
309+
many_to_many_relationships=response_data.get("ManyToManyRelationships"),
256310
)
257311

258312
# -------------------------------------------------------------- conversion

src/PowerPlatform/Dataverse/operations/tables.py

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,20 @@ def get(
187187
``many_to_many_relationships``.
188188
:type include_relationships: :class:`bool`
189189
190-
:return: Dictionary containing ``table_schema_name``,
191-
``table_logical_name``, ``entity_set_name``, and ``metadata_id``.
192-
Returns None if the table is not found.
193-
:rtype: :class:`dict` or None
190+
:return: A :class:`~PowerPlatform.Dataverse.models.table_info.TableInfo`
191+
instance containing table metadata, or ``None`` if the table is
192+
not found. Supports dict-like key access for backward
193+
compatibility.
194+
When ``include_columns`` is ``True``, the ``columns`` attribute
195+
contains a list of
196+
:class:`~PowerPlatform.Dataverse.models.table_info.ColumnInfo`
197+
instances. When ``include_relationships`` is ``True``, the
198+
``one_to_many_relationships``, ``many_to_one_relationships``, and
199+
``many_to_many_relationships`` attributes are populated.
200+
Extra properties requested via ``select`` are accessible via
201+
dict-like key access (e.g., ``info["DisplayName"]``).
202+
:rtype: :class:`~PowerPlatform.Dataverse.models.table_info.TableInfo`
203+
or None
194204
195205
Example::
196206
@@ -201,11 +211,13 @@ def get(
201211
202212
# Extended with columns
203213
info = client.tables.get("account", include_columns=True)
204-
for col in info.get("columns", []):
214+
for col in info.columns:
205215
print(f"{col.logical_name} ({col.attribute_type})")
206216
207217
# Extended with relationships
208218
info = client.tables.get("account", include_relationships=True)
219+
for rel in info.one_to_many_relationships:
220+
print(rel["SchemaName"])
209221
"""
210222
# Normalize empty list to None so callers passing select=[] get the
211223
# lightweight path instead of an expensive full-entity-definition fetch.
@@ -232,35 +244,15 @@ def get(
232244
if raw is None:
233245
return None
234246

235-
# Build result dict starting with the standard 4 fields
236-
result: Dict[str, Any] = {
237-
"table_schema_name": raw.get("SchemaName", table),
238-
"table_logical_name": raw.get("LogicalName"),
239-
"entity_set_name": raw.get("EntitySetName"),
240-
"metadata_id": raw.get("MetadataId"),
241-
}
247+
info = TableInfo.from_api_response(raw)
242248

243-
# Include any extra selected entity properties
249+
# Store any extra selected entity properties
244250
if select:
245251
for prop in select:
246252
if prop not in ("SchemaName", "LogicalName", "EntitySetName", "MetadataId"):
247-
result[prop] = raw.get(prop)
248-
249-
# Convert expanded Attributes into ColumnInfo instances
250-
if include_columns and "Attributes" in raw:
251-
result["columns"] = [ColumnInfo.from_api_response(a) for a in raw["Attributes"]]
252-
253-
# Include expanded relationship collections as raw dicts
254-
if include_relationships:
255-
for raw_key, result_key in (
256-
("OneToManyRelationships", "one_to_many_relationships"),
257-
("ManyToOneRelationships", "many_to_one_relationships"),
258-
("ManyToManyRelationships", "many_to_many_relationships"),
259-
):
260-
if raw_key in raw:
261-
result[result_key] = raw[raw_key]
253+
info._extra[prop] = raw.get(prop)
262254

263-
return result
255+
return info
264256

265257
# -------------------------------------------------------------- get_columns
266258

@@ -382,7 +374,8 @@ def get_column_options(
382374
383375
:return: Option set information with available choices, or ``None`` if
384376
the column is not a Picklist, MultiSelect, Boolean, Status, or
385-
State type.
377+
State type. Also returns ``None`` if the table or column does not
378+
exist (404).
386379
:rtype: :class:`~PowerPlatform.Dataverse.models.table_info.OptionSetInfo`
387380
or None
388381

0 commit comments

Comments
 (0)