Skip to content

Commit d3ab4cf

Browse files
tpellissierclaude
andcommitted
Add RelationshipInfo typed return model for relationship methods
Introduces RelationshipInfo dataclass as the return type for all relationship operations (create_one_to_many_relationship, create_many_to_many_relationship, get_relationship, create_lookup_field). Factory methods: from_one_to_many(), from_many_to_many(), from_api_response() which detects 1:N vs N:N from @odata.type in API responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4a828ac commit d3ab4cf

4 files changed

Lines changed: 383 additions & 32 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""Typed return model for relationship metadata."""
5+
6+
from __future__ import annotations
7+
8+
from dataclasses import dataclass
9+
from typing import Any, Dict, Optional
10+
11+
__all__ = ["RelationshipInfo"]
12+
13+
_OTM_ODATA_TYPE = "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata"
14+
_MTM_ODATA_TYPE = "Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata"
15+
16+
17+
@dataclass
18+
class RelationshipInfo:
19+
"""Typed return model for relationship metadata.
20+
21+
Returned by :meth:`~PowerPlatform.Dataverse.operations.tables.TableOperations.create_one_to_many_relationship`,
22+
:meth:`~PowerPlatform.Dataverse.operations.tables.TableOperations.create_many_to_many_relationship`,
23+
:meth:`~PowerPlatform.Dataverse.operations.tables.TableOperations.get_relationship`, and
24+
:meth:`~PowerPlatform.Dataverse.operations.tables.TableOperations.create_lookup_field`.
25+
26+
:param relationship_id: Relationship metadata GUID.
27+
:type relationship_id: :class:`str` or None
28+
:param relationship_schema_name: Relationship schema name.
29+
:type relationship_schema_name: :class:`str`
30+
:param relationship_type: Either ``"one_to_many"`` or ``"many_to_many"``.
31+
:type relationship_type: :class:`str`
32+
:param lookup_schema_name: Lookup field schema name (one-to-many only).
33+
:type lookup_schema_name: :class:`str` or None
34+
:param referenced_entity: Parent entity logical name (one-to-many only).
35+
:type referenced_entity: :class:`str` or None
36+
:param referencing_entity: Child entity logical name (one-to-many only).
37+
:type referencing_entity: :class:`str` or None
38+
:param entity1_logical_name: First entity logical name (many-to-many only).
39+
:type entity1_logical_name: :class:`str` or None
40+
:param entity2_logical_name: Second entity logical name (many-to-many only).
41+
:type entity2_logical_name: :class:`str` or None
42+
43+
Example::
44+
45+
result = client.tables.create_one_to_many_relationship(lookup, relationship)
46+
print(result.relationship_schema_name)
47+
print(result.lookup_schema_name)
48+
"""
49+
50+
relationship_id: Optional[str] = None
51+
relationship_schema_name: str = ""
52+
relationship_type: str = ""
53+
54+
# One-to-many specific
55+
lookup_schema_name: Optional[str] = None
56+
referenced_entity: Optional[str] = None
57+
referencing_entity: Optional[str] = None
58+
59+
# Many-to-many specific
60+
entity1_logical_name: Optional[str] = None
61+
entity2_logical_name: Optional[str] = None
62+
63+
@classmethod
64+
def from_one_to_many(
65+
cls,
66+
*,
67+
relationship_id: Optional[str],
68+
relationship_schema_name: str,
69+
lookup_schema_name: str,
70+
referenced_entity: str,
71+
referencing_entity: str,
72+
) -> RelationshipInfo:
73+
"""Create from a one-to-many relationship result.
74+
75+
:param relationship_id: Relationship metadata GUID.
76+
:type relationship_id: :class:`str` or None
77+
:param relationship_schema_name: Relationship schema name.
78+
:type relationship_schema_name: :class:`str`
79+
:param lookup_schema_name: Lookup field schema name.
80+
:type lookup_schema_name: :class:`str`
81+
:param referenced_entity: Parent entity logical name.
82+
:type referenced_entity: :class:`str`
83+
:param referencing_entity: Child entity logical name.
84+
:type referencing_entity: :class:`str`
85+
:rtype: :class:`RelationshipInfo`
86+
"""
87+
return cls(
88+
relationship_id=relationship_id,
89+
relationship_schema_name=relationship_schema_name,
90+
relationship_type="one_to_many",
91+
lookup_schema_name=lookup_schema_name,
92+
referenced_entity=referenced_entity,
93+
referencing_entity=referencing_entity,
94+
)
95+
96+
@classmethod
97+
def from_many_to_many(
98+
cls,
99+
*,
100+
relationship_id: Optional[str],
101+
relationship_schema_name: str,
102+
entity1_logical_name: str,
103+
entity2_logical_name: str,
104+
) -> RelationshipInfo:
105+
"""Create from a many-to-many relationship result.
106+
107+
:param relationship_id: Relationship metadata GUID.
108+
:type relationship_id: :class:`str` or None
109+
:param relationship_schema_name: Relationship schema name.
110+
:type relationship_schema_name: :class:`str`
111+
:param entity1_logical_name: First entity logical name.
112+
:type entity1_logical_name: :class:`str`
113+
:param entity2_logical_name: Second entity logical name.
114+
:type entity2_logical_name: :class:`str`
115+
:rtype: :class:`RelationshipInfo`
116+
"""
117+
return cls(
118+
relationship_id=relationship_id,
119+
relationship_schema_name=relationship_schema_name,
120+
relationship_type="many_to_many",
121+
entity1_logical_name=entity1_logical_name,
122+
entity2_logical_name=entity2_logical_name,
123+
)
124+
125+
@classmethod
126+
def from_api_response(cls, response_data: Dict[str, Any]) -> RelationshipInfo:
127+
"""Create from a raw Dataverse Web API response.
128+
129+
Detects one-to-many vs many-to-many from the ``@odata.type`` field
130+
in the response and maps PascalCase keys to snake_case attributes.
131+
132+
:param response_data: Raw relationship metadata from the Web API.
133+
:type response_data: :class:`dict`
134+
:rtype: :class:`RelationshipInfo`
135+
"""
136+
odata_type = response_data.get("@odata.type", "")
137+
rel_id = response_data.get("MetadataId")
138+
schema_name = response_data.get("SchemaName", "")
139+
140+
if _OTM_ODATA_TYPE in odata_type:
141+
return cls(
142+
relationship_id=rel_id,
143+
relationship_schema_name=schema_name,
144+
relationship_type="one_to_many",
145+
referenced_entity=response_data.get("ReferencedEntity"),
146+
referencing_entity=response_data.get("ReferencingEntity"),
147+
lookup_schema_name=response_data.get("ReferencingEntityNavigationPropertyName"),
148+
)
149+
150+
if _MTM_ODATA_TYPE in odata_type:
151+
return cls(
152+
relationship_id=rel_id,
153+
relationship_schema_name=schema_name,
154+
relationship_type="many_to_many",
155+
entity1_logical_name=response_data.get("Entity1LogicalName"),
156+
entity2_logical_name=response_data.get("Entity2LogicalName"),
157+
)
158+
159+
# Fallback: unknown type, populate what we can
160+
return cls(
161+
relationship_id=rel_id,
162+
relationship_schema_name=schema_name,
163+
)

src/PowerPlatform/Dataverse/operations/tables.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CascadeConfiguration,
1717
)
1818
from ..common.constants import CASCADE_BEHAVIOR_REMOVE_LINK
19+
from ..models.relationship_info import RelationshipInfo
1920

2021
if TYPE_CHECKING:
2122
from ..client import DataverseClient
@@ -268,7 +269,7 @@ def create_one_to_many_relationship(
268269
relationship: OneToManyRelationshipMetadata,
269270
*,
270271
solution: Optional[str] = None,
271-
) -> Dict[str, Any]:
272+
) -> RelationshipInfo:
272273
"""Create a one-to-many relationship between tables.
273274
274275
This operation creates both the relationship and the lookup attribute
@@ -281,9 +282,9 @@ def create_one_to_many_relationship(
281282
:param solution: Optional solution unique name to add relationship to.
282283
:type solution: :class:`str` or None
283284
284-
:return: Dictionary with ``relationship_id``, ``lookup_schema_name``,
285-
and related metadata.
286-
:rtype: :class:`dict`
285+
:return: Relationship metadata with ``relationship_id``,
286+
``lookup_schema_name``, and entity names.
287+
:rtype: :class:`~PowerPlatform.Dataverse.models.relationship_info.RelationshipInfo`
287288
288289
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
289290
If the Web API request fails.
@@ -322,14 +323,21 @@ def create_one_to_many_relationship(
322323
)
323324
324325
result = client.tables.create_one_to_many_relationship(lookup, relationship)
325-
print(f"Created lookup field: {result['lookup_schema_name']}")
326+
print(f"Created lookup field: {result.lookup_schema_name}")
326327
"""
327328
with self._client._scoped_odata() as od:
328-
return od._create_one_to_many_relationship(
329+
raw = od._create_one_to_many_relationship(
329330
lookup,
330331
relationship,
331332
solution,
332333
)
334+
return RelationshipInfo.from_one_to_many(
335+
relationship_id=raw.get("relationship_id"),
336+
relationship_schema_name=raw.get("relationship_schema_name", ""),
337+
lookup_schema_name=raw.get("lookup_schema_name", ""),
338+
referenced_entity=raw.get("referenced_entity", ""),
339+
referencing_entity=raw.get("referencing_entity", ""),
340+
)
333341

334342
# ----------------------------------------------------- create_many_to_many
335343

@@ -338,7 +346,7 @@ def create_many_to_many_relationship(
338346
relationship: ManyToManyRelationshipMetadata,
339347
*,
340348
solution: Optional[str] = None,
341-
) -> Dict[str, Any]:
349+
) -> RelationshipInfo:
342350
"""Create a many-to-many relationship between tables.
343351
344352
This operation creates a many-to-many relationship and an intersect
@@ -349,9 +357,9 @@ def create_many_to_many_relationship(
349357
:param solution: Optional solution unique name to add relationship to.
350358
:type solution: :class:`str` or None
351359
352-
:return: Dictionary with ``relationship_id``,
360+
:return: Relationship metadata with ``relationship_id``,
353361
``relationship_schema_name``, and entity names.
354-
:rtype: :class:`dict`
362+
:rtype: :class:`~PowerPlatform.Dataverse.models.relationship_info.RelationshipInfo`
355363
356364
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
357365
If the Web API request fails.
@@ -370,13 +378,19 @@ def create_many_to_many_relationship(
370378
)
371379
372380
result = client.tables.create_many_to_many_relationship(relationship)
373-
print(f"Created: {result['relationship_schema_name']}")
381+
print(f"Created: {result.relationship_schema_name}")
374382
"""
375383
with self._client._scoped_odata() as od:
376-
return od._create_many_to_many_relationship(
384+
raw = od._create_many_to_many_relationship(
377385
relationship,
378386
solution,
379387
)
388+
return RelationshipInfo.from_many_to_many(
389+
relationship_id=raw.get("relationship_id"),
390+
relationship_schema_name=raw.get("relationship_schema_name", ""),
391+
entity1_logical_name=raw.get("entity1_logical_name", ""),
392+
entity2_logical_name=raw.get("entity2_logical_name", ""),
393+
)
380394

381395
# ------------------------------------------------------- delete_relationship
382396

@@ -404,14 +418,15 @@ def delete_relationship(self, relationship_id: str) -> None:
404418

405419
# -------------------------------------------------------- get_relationship
406420

407-
def get_relationship(self, schema_name: str) -> Optional[Dict[str, Any]]:
421+
def get_relationship(self, schema_name: str) -> Optional[RelationshipInfo]:
408422
"""Retrieve relationship metadata by schema name.
409423
410424
:param schema_name: The schema name of the relationship.
411425
:type schema_name: :class:`str`
412426
413-
:return: Relationship metadata dictionary, or None if not found.
414-
:rtype: :class:`dict` or None
427+
:return: Relationship metadata, or ``None`` if not found.
428+
:rtype: :class:`~PowerPlatform.Dataverse.models.relationship_info.RelationshipInfo`
429+
or None
415430
416431
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
417432
If the Web API request fails.
@@ -420,10 +435,13 @@ def get_relationship(self, schema_name: str) -> Optional[Dict[str, Any]]:
420435
421436
rel = client.tables.get_relationship("new_Department_Employee")
422437
if rel:
423-
print(f"Found: {rel['SchemaName']}")
438+
print(f"Found: {rel.relationship_schema_name}")
424439
"""
425440
with self._client._scoped_odata() as od:
426-
return od._get_relationship(schema_name)
441+
raw = od._get_relationship(schema_name)
442+
if raw is None:
443+
return None
444+
return RelationshipInfo.from_api_response(raw)
427445

428446
# ------------------------------------------------------- create_lookup_field
429447

@@ -439,7 +457,7 @@ def create_lookup_field(
439457
cascade_delete: str = CASCADE_BEHAVIOR_REMOVE_LINK,
440458
solution: Optional[str] = None,
441459
language_code: int = 1033,
442-
) -> Dict[str, Any]:
460+
) -> RelationshipInfo:
443461
"""Create a simple lookup field relationship.
444462
445463
This is a convenience method that wraps :meth:`create_one_to_many_relationship`
@@ -471,9 +489,9 @@ def create_lookup_field(
471489
(English).
472490
:type language_code: :class:`int`
473491
474-
:return: Dictionary with ``relationship_id``, ``lookup_schema_name``,
475-
and related metadata.
476-
:rtype: :class:`dict`
492+
:return: Relationship metadata with ``relationship_id``,
493+
``lookup_schema_name``, and entity names.
494+
:rtype: :class:`~PowerPlatform.Dataverse.models.relationship_info.RelationshipInfo`
477495
478496
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
479497
If the Web API request fails.

0 commit comments

Comments
 (0)